Add files via upload

This commit is contained in:
公明
2026-06-30 14:38:49 +08:00
committed by GitHub
parent ded8ac5a3f
commit 2fc37fefd1
+60 -38
View File
@@ -3367,7 +3367,7 @@ function createConversationListItem(conversation) {
// 处理历史记录搜索 // 处理历史记录搜索
let conversationSearchTimer = null; let conversationSearchTimer = null;
function handleConversationSearch(query) { function handleConversationSearch(query) {
conversationsPagination.page = 1; commitConversationsPage(1, { bumpNavigateGen: true });
conversationsSearchQuery = query || ''; conversationsSearchQuery = query || '';
// 防抖处理,避免频繁请求 // 防抖处理,避免频繁请求
if (conversationSearchTimer) { if (conversationSearchTimer) {
@@ -3402,7 +3402,7 @@ function clearConversationSearch() {
clearBtn.style.display = 'none'; clearBtn.style.display = 'none';
} }
conversationsPagination.page = 1; commitConversationsPage(1, { bumpNavigateGen: true });
conversationsSearchQuery = ''; conversationsSearchQuery = '';
loadConversations(''); loadConversations('');
} }
@@ -6303,7 +6303,7 @@ async function refreshConversationProjectFilter() {
function onConversationProjectFilterChange(projectId) { function onConversationProjectFilterChange(projectId) {
setConversationProjectFilter(projectId || ''); setConversationProjectFilter(projectId || '');
conversationsPagination.page = 1; commitConversationsPage(1, { bumpNavigateGen: true });
loadConversationsWithGroups(conversationsSearchQuery); loadConversationsWithGroups(conversationsSearchQuery);
} }
@@ -6410,7 +6410,7 @@ function setConversationSortBy(sortBy) {
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
updateConversationSortMenuUI(); updateConversationSortMenuUI();
closeConversationSortMenu(); closeConversationSortMenu();
conversationsPagination.page = 1; commitConversationsPage(1, { bumpNavigateGen: true });
loadConversationsWithGroups(conversationsSearchQuery); loadConversationsWithGroups(conversationsSearchQuery);
} }
@@ -6452,22 +6452,32 @@ function getConversationsTotalPages() {
return Math.max(1, Math.ceil((total || 0) / pageSize) || 1); return Math.max(1, Math.ceil((total || 0) / pageSize) || 1);
} }
function isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart) { /**
* 分页状态约定
* - conversationsPagination.page 仅在此处用户操作 / reconcile 钳制 / clamp写入
* - loadConversationsWithGroups 只读页码 intentPage 或当前 page 计算 offset
* - isStaleConversationListLoad 丢弃页码或 navigateGen 已变的在途请求
*/
function commitConversationsPage(page, { bumpNavigateGen = false } = {}) {
const next = Math.max(1, parseInt(page, 10) || 1);
if (bumpNavigateGen) {
conversationsListNavigateGen += 1;
}
conversationsPagination.page = next;
return next;
}
function isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart, activePage) {
if (loadSeq !== conversationsListLoadSeq) return true; if (loadSeq !== conversationsListLoadSeq) return true;
// 后台刷新期间用户已翻页(含 2→1、1→2),丢弃过期结果 // 后台刷新期间用户已翻页(含 2→1、1→2),丢弃过期结果
if (intentPage == null && navigateGenAtStart !== conversationsListNavigateGen) return true; if (intentPage == null && navigateGenAtStart !== conversationsListNavigateGen) return true;
// 用户主动翻页后,丢弃目标页已变化的请求
if (intentPage != null && intentPage !== conversationsPagination.page) return true;
// 后台刷新完成时页码已变(如 reconcile 钳制),丢弃过期结果
if (intentPage == null && activePage != null && activePage !== conversationsPagination.page) return true;
return false; return false;
} }
function commitConversationsListPage(activePage, intentPage) {
if (intentPage != null) {
conversationsPagination.page = activePage;
return;
}
// 勿用加载开始时的旧页码覆盖用户中途翻页
if (conversationsPagination.page !== activePage) return;
}
function reconcileConversationsPageAfterTotal(activePage, intentPage, parsed, pageSize, offset, resolvedTotal) { function reconcileConversationsPageAfterTotal(activePage, intentPage, parsed, pageSize, offset, resolvedTotal) {
let total = resolvedTotal; let total = resolvedTotal;
const totalPages = () => Math.max(1, Math.ceil((total || 0) / pageSize) || 1); const totalPages = () => Math.max(1, Math.ceil((total || 0) / pageSize) || 1);
@@ -6478,32 +6488,35 @@ function reconcileConversationsPageAfterTotal(activePage, intentPage, parsed, pa
const serverTotal = parseListTotalValue(parsed.total, parsed.items.length); const serverTotal = parseListTotalValue(parsed.total, parsed.items.length);
const hasPageData = parsed.items.length > 0; const hasPageData = parsed.items.length > 0;
const knownTotal = conversationsPagination.total || 0;
// 用户主动翻页且服务端确有该页数据时,不信过期/偏低的 total(避免 2>1 被钳回第 1 页) // 用户主动翻页且服务端确有该页数据时,不信过期/偏低的 total(避免 2>1 被钳回第 1 页)
if (intentPage != null && (hasPageData || serverTotal > offset || total > offset)) { if (intentPage != null && (hasPageData || serverTotal > offset || total > offset || knownTotal > offset)) {
total = Math.max(total, serverTotal, offset + parsed.items.length); total = Math.max(total, serverTotal, knownTotal, offset + parsed.items.length);
if (activePage <= totalPages()) { if (activePage <= totalPages()) {
return { ok: true, total }; return { ok: true, total };
} }
} }
const clampedPage = totalPages(); const clampedPage = totalPages();
conversationsPagination.page = clampedPage; commitConversationsPage(clampedPage);
return { ok: false, total, clampedPage }; return { ok: false, total, clampedPage };
} }
function clampConversationsPageToTotal() { function clampConversationsPageToTotal() {
const totalPages = getConversationsTotalPages(); const totalPages = getConversationsTotalPages();
if (conversationsPagination.page > totalPages) { if (conversationsPagination.page > totalPages) {
conversationsPagination.page = totalPages; commitConversationsPage(totalPages);
return true; return true;
} }
if (conversationsPagination.page < 1) { if (conversationsPagination.page < 1) {
conversationsPagination.page = 1; commitConversationsPage(1);
return true; return true;
} }
return false; return false;
} }
let conversationsPaginationRenderLock = false;
function initConversationsPaginationEvents() { function initConversationsPaginationEvents() {
if (conversationsPaginationEventsBound) return; if (conversationsPaginationEventsBound) return;
const el = document.getElementById('conversations-pagination'); const el = document.getElementById('conversations-pagination');
@@ -6518,6 +6531,12 @@ function initConversationsPaginationEvents() {
goConversationsPage(page); goConversationsPage(page);
} }
}); });
el.addEventListener('change', (e) => {
if (conversationsPaginationRenderLock) return;
if (e.target && e.target.id === 'conversations-page-size-pagination') {
changeConversationsPageSize();
}
});
} }
function parseListTotalValue(raw, itemsLength) { function parseListTotalValue(raw, itemsLength) {
@@ -6641,7 +6660,9 @@ function renderConversationsPagination(visibleCount) {
const nextLabel = tFn ? tFn('chat.paginationNext') : 'Next'; const nextLabel = tFn ? tFn('chat.paginationNext') : 'Next';
const prevPage = page - 1; const prevPage = page - 1;
const nextPage = page + 1; const nextPage = page + 1;
el.innerHTML = ` conversationsPaginationRenderLock = true;
try {
el.innerHTML = `
<div class="sidebar-list-pagination-inner sidebar-list-pagination-inner--compact"> <div class="sidebar-list-pagination-inner sidebar-list-pagination-inner--compact">
<span class="pagination-info">${escapeHtml(infoText)}</span> <span class="pagination-info">${escapeHtml(infoText)}</span>
<div class="pagination-controls"> <div class="pagination-controls">
@@ -6651,21 +6672,22 @@ function renderConversationsPagination(visibleCount) {
</div> </div>
<label class="pagination-page-size"> <label class="pagination-page-size">
${escapeHtml(perPageLabel)} ${escapeHtml(perPageLabel)}
<select id="conversations-page-size-pagination" onchange="changeConversationsPageSize()"> <select id="conversations-page-size-pagination">
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option> <option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
<option value="50" ${pageSize === 50 ? 'selected' : ''}>50</option> <option value="50" ${pageSize === 50 ? 'selected' : ''}>50</option>
<option value="100" ${pageSize === 100 ? 'selected' : ''}>100</option> <option value="100" ${pageSize === 100 ? 'selected' : ''}>100</option>
</select> </select>
</label> </label>
</div>`; </div>`;
} finally {
conversationsPaginationRenderLock = false;
}
} }
function goConversationsPage(page) { function goConversationsPage(page) {
const requestedPage = Math.max(1, parseInt(page, 10) || 1); const requestedPage = Math.max(1, parseInt(page, 10) || 1);
const scrollToTop = requestedPage !== conversationsPagination.page; const scrollToTop = requestedPage !== conversationsPagination.page;
conversationsListNavigateGen += 1; commitConversationsPage(requestedPage, { bumpNavigateGen: true });
conversationsPagination.page = requestedPage;
// intentPage:用户主动翻页,不在此处用可能已被并发刷新污染的 total 做钳制
loadConversationsWithGroups(conversationsSearchQuery, { loadConversationsWithGroups(conversationsSearchQuery, {
refreshMeta: false, refreshMeta: false,
scrollToTop, scrollToTop,
@@ -6681,7 +6703,7 @@ function changeConversationsPageSize() {
localStorage.setItem(CONVERSATIONS_PAGE_SIZE_KEY, String(newSize)); localStorage.setItem(CONVERSATIONS_PAGE_SIZE_KEY, String(newSize));
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
conversationsPagination.pageSize = newSize; conversationsPagination.pageSize = newSize;
conversationsPagination.page = 1; commitConversationsPage(1, { bumpNavigateGen: true });
loadConversationsWithGroups(conversationsSearchQuery); loadConversationsWithGroups(conversationsSearchQuery);
} }
@@ -6791,9 +6813,6 @@ async function loadConversationsWithGroups(searchQuery = '', options = {}) {
const pageSize = getConversationsPageSize(); const pageSize = getConversationsPageSize();
conversationsPagination.pageSize = pageSize; conversationsPagination.pageSize = pageSize;
const activePage = intentPage != null ? intentPage : conversationsPagination.page; const activePage = intentPage != null ? intentPage : conversationsPagination.page;
if (intentPage != null) {
conversationsPagination.page = intentPage;
}
const offset = (activePage - 1) * pageSize; const offset = (activePage - 1) * pageSize;
const convParams = new URLSearchParams({ limit: String(pageSize), offset: String(offset) }); const convParams = new URLSearchParams({ limit: String(pageSize), offset: String(offset) });
if (conversationSortBy === 'created_at') { if (conversationSortBy === 'created_at') {
@@ -6816,7 +6835,7 @@ async function loadConversationsWithGroups(searchQuery = '', options = {}) {
} }
const results = await Promise.all(fetchTasks); const results = await Promise.all(fetchTasks);
const response = results[results.length - 1]; const response = results[results.length - 1];
if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart)) return; if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart, activePage)) return;
const listContainer = document.getElementById('conversations-list'); const listContainer = document.getElementById('conversations-list');
if (!listContainer) { if (!listContainer) {
@@ -6839,10 +6858,10 @@ async function loadConversationsWithGroups(searchQuery = '', options = {}) {
} }
const data = await response.json(); const data = await response.json();
if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart)) return; if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart, activePage)) return;
const parsed = parseConversationsListResponse(data); const parsed = parseConversationsListResponse(data);
const resolvedTotal = await resolveConversationsListTotal(convParams, parsed, pageSize, offset); const resolvedTotal = await resolveConversationsListTotal(convParams, parsed, pageSize, offset);
if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart)) return; if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart, activePage)) return;
conversationsPagination.total = resolvedTotal; conversationsPagination.total = resolvedTotal;
const pageCheck = reconcileConversationsPageAfterTotal( const pageCheck = reconcileConversationsPageAfterTotal(
@@ -6850,17 +6869,20 @@ async function loadConversationsWithGroups(searchQuery = '', options = {}) {
); );
conversationsPagination.total = pageCheck.total; conversationsPagination.total = pageCheck.total;
if (!pageCheck.ok) { if (!pageCheck.ok) {
if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart)) return; if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart, activePage)) return;
// 用户主动翻页被钳制时仍保留 intent,并 bump navigateGen 使在途后台刷新失效
if (intentPage != null) {
commitConversationsPage(pageCheck.clampedPage, { bumpNavigateGen: true });
}
loadConversationsWithGroups(searchQuery, { loadConversationsWithGroups(searchQuery, {
...options, ...options,
intentPage: null, intentPage: pageCheck.clampedPage,
scrollToTop: options.scrollToTop === true || activePage !== pageCheck.clampedPage, scrollToTop: options.scrollToTop === true || activePage !== pageCheck.clampedPage,
}); });
return; return;
} }
commitConversationsListPage(activePage, intentPage);
if (intentPage == null && clampConversationsPageToTotal()) { if (intentPage == null && clampConversationsPageToTotal()) {
if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart)) return; if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart, activePage)) return;
loadConversationsWithGroups(searchQuery, options); loadConversationsWithGroups(searchQuery, options);
return; return;
} }
@@ -7007,7 +7029,7 @@ async function loadConversationsWithGroups(searchQuery = '', options = {}) {
return; return;
} }
if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart)) return; if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart, activePage)) return;
listContainer.appendChild(fragment); listContainer.appendChild(fragment);
updateActiveConversation(); updateActiveConversation();
renderConversationsPagination(visibleCount); renderConversationsPagination(visibleCount);
@@ -7015,13 +7037,13 @@ async function loadConversationsWithGroups(searchQuery = '', options = {}) {
// 翻页时回到列表顶部;后台刷新保留滚动位置 // 翻页时回到列表顶部;后台刷新保留滚动位置
if (sidebarContent) { if (sidebarContent) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart)) { if (!isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart, activePage)) {
sidebarContent.scrollTop = scrollToTop ? 0 : savedScrollTop; sidebarContent.scrollTop = scrollToTop ? 0 : savedScrollTop;
} }
}); });
} }
} catch (error) { } catch (error) {
if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart)) return; if (isStaleConversationListLoad(loadSeq, intentPage, navigateGenAtStart, activePage)) return;
console.error('加载对话列表失败:', error); console.error('加载对话列表失败:', error);
// 错误时显示空状态,而不是错误提示(更友好的用户体验) // 错误时显示空状态,而不是错误提示(更友好的用户体验)
const listContainer = document.getElementById('conversations-list'); const listContainer = document.getElementById('conversations-list');