refactor: standardize pagination response structure and enhance cursor handling
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 5m8s
Frontend CI / ota-android (push) Successful in 11m22s
Frontend CI / build-android-apk (push) Successful in 45m46s

- Update various services and hooks to replace 'items' with 'list' for consistency in pagination responses.
- Modify PostRepository, commentService, groupService, messageService, and postService to align with the new response structure.
- Enhance cursor pagination logic in hooks and components to ensure proper handling of pagination states.
- Refactor HomeScreen, PostDetailScreen, SearchScreen, and other components to utilize the updated pagination structure.
This commit is contained in:
lafay
2026-03-23 03:58:26 +08:00
parent 88510aa6ae
commit 26ae288470
19 changed files with 312 additions and 309 deletions

View File

@@ -4,13 +4,12 @@
* 集成 ProcessPostUseCase 进行状态管理
*/
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import { useState, useRef, useCallback, useEffect } from 'react';
import {
PostIdentifier,
PostUpdate,
PostUpdateType,
PostDiffConfig,
PostDiffResult,
} from '../infrastructure/diff/postTypes';
import {
PostDiffCalculator,
@@ -119,7 +118,6 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
enableDiff = true,
enableBatching = true,
maxPostCount = 10000,
changeRatioThreshold = 0.5,
listKey = 'default',
autoSubscribe = true,
} = options;
@@ -147,37 +145,7 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
const previousPostsRef = useRef<T[]>(initialPosts);
const unsubscribeRef = useRef<(() => void) | null>(null);
// ==================== 初始化 ====================
// 初始化差异计算器
useEffect(() => {
if (enableDiff) {
calculatorRef.current = createPostDiffCalculator<T>(diffConfig);
}
return () => {
calculatorRef.current = null;
};
}, [enableDiff, diffConfig]);
// 初始化批量处理器
useEffect(() => {
if (enableBatching) {
batcherRef.current = createPostUpdateBatcher(batcherOptions);
// 订阅批量更新
const unsubscribe = batcherRef.current.subscribe((updates) => {
handleBatchUpdates(updates);
});
return () => {
unsubscribe();
batcherRef.current?.destroy();
batcherRef.current = null;
};
}
}, [enableBatching, batcherOptions]);
// ==================== 批量更新处理 ====================
// ==================== 批量更新处理(须在订阅 batcher 的 useEffect 之前定义)====================
/**
* 处理批量更新
@@ -289,123 +257,34 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
}
}, []);
// ==================== 差异计算 ====================
// ==================== 初始化 ====================
/**
* 计算并应用差异
*/
const calculateAndApplyDiff = useCallback((newPosts: T[]) => {
const previousPosts = previousPostsRef.current;
// 如果帖子数量变化太大,直接重置
const previousCount = previousPosts.length;
const currentCount = newPosts.length;
const changeRatio = previousCount === 0 ? 1 : Math.abs(currentCount - previousCount) / previousCount;
if (changeRatio > changeRatioThreshold) {
setPosts(newPosts);
previousPostsRef.current = newPosts;
return;
// 初始化差异计算器
useEffect(() => {
if (enableDiff) {
calculatorRef.current = createPostDiffCalculator<T>(diffConfig);
}
return () => {
calculatorRef.current = null;
};
}, [enableDiff, diffConfig]);
// 使用差异计算
if (enableDiff && calculatorRef.current) {
const diff = calculatorRef.current.calculateDiff(previousPosts, newPosts);
// 初始化批量处理器
useEffect(() => {
if (!enableBatching) return;
if (diff.shouldReset) {
setPosts(newPosts);
} else {
// 将差异转换为更新操作
const updates: PostUpdate[] = [];
batcherRef.current = createPostUpdateBatcher(batcherOptions);
// 处理新增
if (diff.added.length > 0) {
if (diff.added.length === 1) {
updates.push({
type: PostUpdateType.ADD,
post: diff.added[0],
timestamp: Date.now(),
});
} else {
updates.push({
type: PostUpdateType.BATCH_ADD,
posts: diff.added,
timestamp: Date.now(),
});
}
}
const unsubscribe = batcherRef.current.subscribe((updates) => {
handleBatchUpdates(updates);
});
// 处理更新
if (diff.updated.length > 0) {
if (diff.updated.length === 1) {
updates.push({
type: PostUpdateType.UPDATE,
postId: diff.updated[0].post.id,
updates: diff.updated[0].changes,
timestamp: Date.now(),
});
} else {
updates.push({
type: PostUpdateType.BATCH_UPDATE,
updates: diff.updated.map(u => ({
postId: u.post.id,
changes: u.changes,
})),
timestamp: Date.now(),
});
}
}
// 处理删除
if (diff.deleted.length > 0) {
if (diff.deleted.length === 1) {
updates.push({
type: PostUpdateType.DELETE,
postId: diff.deleted[0].post.id,
timestamp: Date.now(),
});
} else {
updates.push({
type: PostUpdateType.BATCH_DELETE,
postIds: diff.deleted.map(d => d.post.id),
timestamp: Date.now(),
});
}
}
// 处理移动
if (diff.moved.length > 0) {
for (const move of diff.moved) {
updates.push({
type: PostUpdateType.MOVE,
postId: move.post.id,
toIndex: move.toIndex,
timestamp: Date.now(),
});
}
}
// 应用更新
if (updates.length > 0) {
if (enableBatching && batcherRef.current) {
batcherRef.current.addUpdates(updates);
} else {
handleBatchUpdates(updates);
}
}
}
} else {
// 不使用差异计算,直接设置
setPosts(newPosts);
}
// 更新帖子数量限制检查
if (newPosts.length > maxPostCount) {
console.warn(`[useDifferentialPosts] Post count (${newPosts.length}) exceeds limit (${maxPostCount})`);
}
previousPostsRef.current = newPosts;
}, [enableDiff, enableBatching, maxPostCount, changeRatioThreshold, handleBatchUpdates]);
return () => {
unsubscribe();
batcherRef.current?.destroy();
batcherRef.current = null;
};
}, [enableBatching, batcherOptions, handleBatchUpdates]);
// ==================== UseCase 订阅 ====================
@@ -415,26 +294,39 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
const handleUseCaseEvent = useCallback((event: PostUseCaseEvent) => {
switch (event.type) {
case 'state_changed': {
const { state } = event.payload as { key: string; state: PostsState };
const { key: eventKey, state } = event.payload as { key: string; state: PostsState };
// 全局订阅:只应用当前列表 key避免其他 Tab/列表的 state 覆盖本列表(如 createPost 会更新所有 key
if (eventKey !== listKey) {
break;
}
if (state) {
setLoading(state.isLoading);
setRefreshing(state.isRefreshing);
setError(state.error);
setHasMore(state.hasMore);
// 如果帖子列表有变化,进行差异计算
// ProcessPostUseCase 的 state.posts 已是合并后的完整列表。
// 不再走差异计算 + PostUpdateBatcher批量应用晚于 previousPostsRef 更新时,
// BATCH_ADD 会在过短的 currentPosts 上 splice导致连续游标加载「请求成功但列表不增长」。
batcherRef.current?.clearPending();
calculatorRef.current?.reset();
if (state.posts && state.posts.length > 0) {
calculateAndApplyDiff(state.posts as unknown as T[]);
const next = state.posts as unknown as T[];
if (next.length > maxPostCount) {
console.warn(`[useDifferentialPosts] Post count (${next.length}) exceeds limit (${maxPostCount})`);
}
setPosts(next);
previousPostsRef.current = next;
} else {
setPosts([]);
previousPostsRef.current = [];
}
}
break;
}
case 'posts_loaded': {
const { posts: loadedPosts } = event.payload;
if (loadedPosts) {
calculateAndApplyDiff(loadedPosts as T[]);
}
// fetchPosts 在 updatePostsState 时已发出 state_changed含合并后 posts此处不再应用避免与其它 key 竞态或重复计算
break;
}
@@ -485,12 +377,17 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
}
case 'error_occurred': {
const { error: errorMsg } = event.payload;
setError(errorMsg);
const payload = event.payload as { key?: string; error?: string };
if (payload.key !== undefined && payload.key !== listKey) {
break;
}
if (payload.error !== undefined) {
setError(payload.error);
}
break;
}
}
}, [calculateAndApplyDiff]);
}, [listKey, maxPostCount]);
// 订阅 UseCase 状态变化
useEffect(() => {