refactor: standardize pagination response structure and enhance cursor handling
- 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:
@@ -314,8 +314,8 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64
|
||||
provenance: false
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-from: type=registry,ref=code.littlelan.cn/carrot_bbs/frontend-web:buildcache
|
||||
cache-to: type=registry,ref=code.littlelan.cn/carrot_bbs/frontend-web:buildcache,mode=max
|
||||
|
||||
- name: Show image tags
|
||||
run: |
|
||||
|
||||
@@ -64,10 +64,14 @@ export interface PostsState {
|
||||
hasMore: boolean;
|
||||
/** 错误信息 */
|
||||
error: string | null;
|
||||
/** 当前游标 */
|
||||
/** 当前游标(cursor 模式) */
|
||||
cursor: string | null;
|
||||
/** 当前页码(page 模式) */
|
||||
currentPage: number;
|
||||
/** 最后加载时间 */
|
||||
lastLoadTime: number | null;
|
||||
/** 上次请求的参数(用于加载更多和刷新) */
|
||||
lastParams?: GetPostsParams;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,6 +202,7 @@ class ProcessPostUseCase {
|
||||
hasMore: true,
|
||||
error: null,
|
||||
cursor: null,
|
||||
currentPage: 1,
|
||||
lastLoadTime: null,
|
||||
};
|
||||
this.postsState.set(key, state);
|
||||
@@ -257,29 +262,32 @@ class ProcessPostUseCase {
|
||||
* @param key 列表键(用于区分不同的列表)
|
||||
*/
|
||||
async fetchPosts(params?: GetPostsParams, key: string = 'default'): Promise<PostsResult> {
|
||||
const state = this.getPostsState(key);
|
||||
|
||||
// 更新加载状态
|
||||
this.updatePostsState(key, { isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
// 合并分页参数
|
||||
// 默认传递空字符串 cursor 来启动 cursor 模式
|
||||
// 注意:cursor 要放在 ...params 后面,这样只有在 params 没有传 cursor 时才使用默认值
|
||||
const queryParams: GetPostsParams = {
|
||||
page: params?.page || 1,
|
||||
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
|
||||
cursor: params?.cursor || state.cursor || undefined,
|
||||
...params,
|
||||
cursor: params?.cursor !== undefined ? params.cursor : '', // 默认使用空字符串启动 cursor 模式
|
||||
};
|
||||
|
||||
const result = await postRepository.getPosts(queryParams);
|
||||
|
||||
// 更新状态
|
||||
// 判断是否为 cursor 模式(有 next_cursor 返回)
|
||||
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
||||
|
||||
// 更新状态(保存请求参数以便加载更多时使用)
|
||||
this.updatePostsState(key, {
|
||||
posts: result.posts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: result.nextCursor || null,
|
||||
cursor: isCursorMode ? result.nextCursor : null, // cursor 模式保存 cursor,否则设为 null 表示 page 模式
|
||||
currentPage: isCursorMode ? undefined : 1, // page 模式从第 1 页开始
|
||||
isLoading: false,
|
||||
lastLoadTime: Date.now(),
|
||||
lastParams: params, // 保存请求参数
|
||||
});
|
||||
|
||||
// 通知订阅者
|
||||
@@ -312,20 +320,29 @@ class ProcessPostUseCase {
|
||||
* @param key 列表键
|
||||
*/
|
||||
async refreshPosts(key: string = 'default'): Promise<PostsResult> {
|
||||
const state = this.getPostsState(key);
|
||||
|
||||
// 更新刷新状态
|
||||
this.updatePostsState(key, { isRefreshing: true, error: null });
|
||||
|
||||
try {
|
||||
// 使用保存的请求参数(如 post_type)进行刷新
|
||||
// 默认传递空字符串 cursor 来启动 cursor 模式
|
||||
const result = await postRepository.getPosts({
|
||||
page: 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
...state.lastParams,
|
||||
cursor: state.lastParams?.cursor !== undefined ? state.lastParams.cursor : '', // 默认使用空字符串启动 cursor 模式
|
||||
});
|
||||
|
||||
// 判断是否为 cursor 模式(有 next_cursor 返回)
|
||||
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
||||
|
||||
// 更新状态
|
||||
this.updatePostsState(key, {
|
||||
posts: result.posts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: result.nextCursor || null,
|
||||
cursor: isCursorMode ? result.nextCursor : null,
|
||||
currentPage: isCursorMode ? undefined : 1,
|
||||
isRefreshing: false,
|
||||
lastLoadTime: Date.now(),
|
||||
});
|
||||
@@ -372,21 +389,45 @@ class ProcessPostUseCase {
|
||||
this.updatePostsState(key, { isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const queryParams: GetPostsParams = {
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
cursor: state.cursor || undefined,
|
||||
};
|
||||
// 判断使用哪种分页模式:cursor 不为 null 表示使用 cursor 模式
|
||||
const useCursorMode = state.cursor !== null && state.cursor !== undefined;
|
||||
|
||||
// 必须先展开 lastParams,再由本次分页字段覆盖。
|
||||
// lastParams 来自首次 fetch(通常含 page:1),若写在展开之前会把 nextPage/cursor 覆盖掉,导致永远请求第 1 页、无法加载第 3 页及以后。
|
||||
const base = { ...(state.lastParams ?? {}) } as GetPostsParams;
|
||||
let queryParams: GetPostsParams;
|
||||
|
||||
if (useCursorMode) {
|
||||
delete base.page;
|
||||
queryParams = {
|
||||
...base,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
cursor: state.cursor!,
|
||||
};
|
||||
} else {
|
||||
const nextPage = (state.currentPage || 1) + 1;
|
||||
delete base.cursor;
|
||||
queryParams = {
|
||||
...base,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
page: nextPage,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await postRepository.getPosts(queryParams);
|
||||
|
||||
// 合并新数据
|
||||
const newPosts = [...state.posts, ...result.posts];
|
||||
|
||||
// 判断响应是否为 cursor 模式(有 next_cursor 返回)
|
||||
const responseIsCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
||||
|
||||
// 更新状态
|
||||
this.updatePostsState(key, {
|
||||
posts: newPosts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: result.nextCursor || null,
|
||||
cursor: responseIsCursorMode ? result.nextCursor : null,
|
||||
currentPage: useCursorMode ? state.currentPage : ((state.currentPage || 1) + 1),
|
||||
isLoading: false,
|
||||
lastLoadTime: Date.now(),
|
||||
});
|
||||
@@ -398,6 +439,7 @@ class ProcessPostUseCase {
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '加载更多帖子失败';
|
||||
console.error('[ProcessPostUseCase] Load more error:', errorMessage);
|
||||
this.updatePostsState(key, {
|
||||
isLoading: false,
|
||||
error: errorMessage,
|
||||
|
||||
@@ -231,9 +231,12 @@ export class PostRepository implements IPostRepository {
|
||||
try {
|
||||
const queryParams: Record<string, any> = {};
|
||||
|
||||
if (params?.page) queryParams.page = params.page;
|
||||
if (params?.page !== undefined) queryParams.page = params.page;
|
||||
if (params?.pageSize) queryParams.page_size = params.pageSize;
|
||||
if (params?.cursor) queryParams.cursor = params.cursor;
|
||||
// 支持 cursor 参数:空字符串也传递,用于启动 cursor 模式
|
||||
if (params?.cursor !== undefined && params?.cursor !== null) {
|
||||
queryParams.cursor = params.cursor;
|
||||
}
|
||||
if (params?.post_type) queryParams.tab = params.post_type;
|
||||
if (params?.communityId) queryParams.community_id = params.communityId;
|
||||
if (params?.authorId) queryParams.author_id = params.authorId;
|
||||
@@ -253,9 +256,16 @@ export class PostRepository implements IPostRepository {
|
||||
this.saveToLocalCache(post);
|
||||
});
|
||||
|
||||
// 计算 hasMore:优先使用 has_more,否则通过 page/total_pages 计算
|
||||
let hasMore = response.has_more;
|
||||
if (hasMore === undefined && response.page !== undefined && response.total_pages !== undefined) {
|
||||
hasMore = response.page < response.total_pages;
|
||||
}
|
||||
hasMore = hasMore ?? false;
|
||||
|
||||
return {
|
||||
posts,
|
||||
hasMore: response.has_more ?? false,
|
||||
hasMore,
|
||||
nextCursor: response.next_cursor,
|
||||
total: response.total,
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useCursorPagination<T, P = void>(
|
||||
const effectivePageSize = Math.min(Math.max(1, pageSize), MAX_PAGE_SIZE);
|
||||
|
||||
const [state, setState] = useState<CursorPaginationState<T>>({
|
||||
items: [],
|
||||
list: [],
|
||||
nextCursor: null,
|
||||
prevCursor: null,
|
||||
hasMore: true, // 初始设为 true,以便首次加载可以执行
|
||||
@@ -86,7 +86,7 @@ export function useCursorPagination<T, P = void>(
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
items: [...prev.items, ...response.items],
|
||||
list: [...prev.list, ...response.list],
|
||||
nextCursor: response.next_cursor,
|
||||
prevCursor: response.prev_cursor,
|
||||
hasMore: response.has_more && response.next_cursor !== null,
|
||||
@@ -134,7 +134,7 @@ export function useCursorPagination<T, P = void>(
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
// 向前加载时,新数据放在前面
|
||||
items: [...response.items, ...prev.items],
|
||||
list: [...response.list, ...prev.list],
|
||||
nextCursor: response.next_cursor,
|
||||
prevCursor: response.prev_cursor,
|
||||
hasMore: response.has_more,
|
||||
@@ -169,7 +169,7 @@ export function useCursorPagination<T, P = void>(
|
||||
if (cancelledRef.current) return;
|
||||
|
||||
setState({
|
||||
items: response.items,
|
||||
list: response.list,
|
||||
nextCursor: response.next_cursor,
|
||||
prevCursor: response.prev_cursor,
|
||||
hasMore: response.has_more && response.next_cursor !== null,
|
||||
@@ -196,7 +196,7 @@ export function useCursorPagination<T, P = void>(
|
||||
isFirstLoadRef.current = true;
|
||||
isLoadingRef.current = false;
|
||||
setState({
|
||||
items: [],
|
||||
list: [],
|
||||
nextCursor: null,
|
||||
prevCursor: null,
|
||||
hasMore: true, // 重置后也设为 true,以便可以重新加载
|
||||
@@ -208,16 +208,16 @@ export function useCursorPagination<T, P = void>(
|
||||
}, []);
|
||||
|
||||
// 设置数据(用于外部数据注入)
|
||||
const setItems = useCallback(
|
||||
const setList = useCallback(
|
||||
(
|
||||
items: T[],
|
||||
list: T[],
|
||||
nextCursor: string | null,
|
||||
prevCursor: string | null,
|
||||
hasMore: boolean
|
||||
) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
items,
|
||||
list,
|
||||
nextCursor,
|
||||
prevCursor,
|
||||
hasMore,
|
||||
@@ -230,7 +230,7 @@ export function useCursorPagination<T, P = void>(
|
||||
// 自动加载第一页
|
||||
useEffect(() => {
|
||||
// 使用 ref 防止重复加载,不依赖 loadMore 函数引用
|
||||
// 只检查 autoLoad 和 autoLoadRef,不再依赖 state.items.length
|
||||
// 只检查 autoLoad 和 autoLoadRef,不再依赖 state.list.length
|
||||
if (!autoLoad || autoLoadRef.current) return;
|
||||
|
||||
autoLoadRef.current = true;
|
||||
@@ -261,7 +261,7 @@ export function useCursorPagination<T, P = void>(
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
items: response.items,
|
||||
list: response.list,
|
||||
nextCursor: response.next_cursor,
|
||||
prevCursor: response.prev_cursor,
|
||||
hasMore: response.has_more && response.next_cursor !== null,
|
||||
@@ -296,7 +296,7 @@ export function useCursorPagination<T, P = void>(
|
||||
loadPrevious,
|
||||
refresh,
|
||||
reset,
|
||||
setItems,
|
||||
setList,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -205,7 +205,7 @@ export interface CursorPaginationConfig {
|
||||
*/
|
||||
export interface CursorPaginationState<T> {
|
||||
/** 数据项列表 */
|
||||
items: T[];
|
||||
list: T[];
|
||||
/** 下一页游标 */
|
||||
nextCursor: string | null;
|
||||
/** 上一页游标 */
|
||||
@@ -235,7 +235,7 @@ export interface CursorPaginationActions<T> {
|
||||
/** 重置状态 */
|
||||
reset: () => void;
|
||||
/** 设置数据(用于外部数据注入) */
|
||||
setItems: (items: T[], nextCursor: string | null, prevCursor: string | null, hasMore: boolean) => void;
|
||||
setList: (list: T[], nextCursor: string | null, prevCursor: string | null, hasMore: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,7 +253,7 @@ export type CursorFetchFunction<T, P = void> = (params: {
|
||||
/** 额外参数 */
|
||||
extraParams?: P;
|
||||
}) => Promise<{
|
||||
items: T[];
|
||||
list: T[];
|
||||
next_cursor: string | null;
|
||||
prev_cursor: string | null;
|
||||
has_more: boolean;
|
||||
|
||||
@@ -89,6 +89,10 @@ export const HomeScreen: React.FC = () => {
|
||||
const postIdsRef = useRef<Set<string>>(new Set());
|
||||
const lastSwipeAtRef = useRef(0);
|
||||
|
||||
// 网格模式滚动位置检测
|
||||
const gridScrollYRef = useRef(0);
|
||||
const isLoadingMoreRef = useRef(false);
|
||||
|
||||
// 构建一个以 postId 为 key 的 map,用于快速查找
|
||||
const postsMap = useMemo(() => {
|
||||
const map = new Map<string, Post>();
|
||||
@@ -131,15 +135,31 @@ export const HomeScreen: React.FC = () => {
|
||||
|
||||
// 加载更多方法
|
||||
const loadMore = useCallback(async () => {
|
||||
if (hasMore && !isLoading) {
|
||||
if (hasMore && !isLoading && !isLoadingMoreRef.current) {
|
||||
isLoadingMoreRef.current = true;
|
||||
try {
|
||||
await processPostUseCase.loadMorePosts(listKey);
|
||||
} catch (err) {
|
||||
console.error('加载更多失败:', err);
|
||||
} finally {
|
||||
isLoadingMoreRef.current = false;
|
||||
}
|
||||
}
|
||||
}, [listKey, hasMore, isLoading]);
|
||||
|
||||
// 网格模式滚动处理 - 检测是否滚动到底部
|
||||
const handleGridScroll = useCallback((event: any) => {
|
||||
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent;
|
||||
const scrollY = contentOffset.y;
|
||||
const visibleHeight = layoutMeasurement.height;
|
||||
const contentHeight = contentSize.height;
|
||||
|
||||
// 当滚动到距离底部 200px 时触发加载更多
|
||||
if (scrollY + visibleHeight >= contentHeight - 200) {
|
||||
loadMore();
|
||||
}
|
||||
}, [loadMore]);
|
||||
|
||||
// 刷新方法 - 先获取正确类型的帖子,再刷新
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
@@ -485,6 +505,7 @@ export const HomeScreen: React.FC = () => {
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
scrollEventThrottle={100}
|
||||
onScroll={handleGridScroll}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
@@ -495,6 +516,12 @@ export const HomeScreen: React.FC = () => {
|
||||
}
|
||||
>
|
||||
{distributePostsToColumns.map((column, index) => renderWaterfallColumn(column, index))}
|
||||
{/* 加载更多指示器 */}
|
||||
{isLoading && displayPosts.length > 0 && (
|
||||
<View style={styles.loadingMore}>
|
||||
<Loading size="sm" />
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -767,4 +794,9 @@ const styles = StyleSheet.create({
|
||||
height: 72,
|
||||
borderRadius: 36,
|
||||
},
|
||||
loadingMore: {
|
||||
paddingVertical: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
// 使用游标分页 Hook 管理评论列表
|
||||
const {
|
||||
items: paginatedComments,
|
||||
list: paginatedComments,
|
||||
isLoading: isCommentsLoading,
|
||||
isRefreshing: isCommentsRefreshing,
|
||||
hasMore: hasMoreComments,
|
||||
@@ -206,13 +206,13 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
// 如果是从评论按钮跳转过来的,加载完成后滚动到评论区
|
||||
useEffect(() => {
|
||||
if (shouldScrollToComments && !loading && comments.length > 0) {
|
||||
if (shouldScrollToComments && !loading && (comments?.length ?? 0) > 0) {
|
||||
// 延迟确保列表已完成渲染和布局
|
||||
setTimeout(() => {
|
||||
flatListRef.current?.scrollToIndex({ index: 0, animated: true, viewPosition: 0 });
|
||||
}, 500);
|
||||
}
|
||||
}, [shouldScrollToComments, loading, comments.length]);
|
||||
}, [shouldScrollToComments, loading, comments?.length]);
|
||||
|
||||
// 动态设置导航头部
|
||||
useEffect(() => {
|
||||
@@ -1413,7 +1413,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
加载更多评论
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : comments.length > 0 ? (
|
||||
) : (comments?.length ?? 0) > 0 ? (
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.noMoreComments}>
|
||||
没有更多评论了
|
||||
</Text>
|
||||
@@ -1546,7 +1546,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
加载更多评论
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : comments.length > 0 ? (
|
||||
) : (comments?.length ?? 0) > 0 ? (
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.noMoreComments}>
|
||||
没有更多评论了
|
||||
</Text>
|
||||
|
||||
@@ -83,7 +83,7 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
|
||||
// 使用游标分页进行帖子搜索
|
||||
const {
|
||||
items: searchResults,
|
||||
list: searchResults,
|
||||
isLoading,
|
||||
isRefreshing,
|
||||
hasMore,
|
||||
@@ -93,7 +93,7 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
} = useCursorPagination<Post, { query: string }>(
|
||||
async ({ cursor, pageSize, extraParams }) => {
|
||||
if (!extraParams?.query) {
|
||||
return { items: [], next_cursor: null, prev_cursor: null, has_more: false };
|
||||
return { list: [], next_cursor: null, prev_cursor: null, has_more: false };
|
||||
}
|
||||
const response = await postService.searchPostsCursor(extraParams.query, {
|
||||
cursor,
|
||||
|
||||
@@ -72,7 +72,7 @@ const GroupMembersScreen: React.FC = () => {
|
||||
|
||||
// 使用游标分页 Hook 管理成员列表
|
||||
const {
|
||||
items: members,
|
||||
list: members,
|
||||
isLoading,
|
||||
isRefreshing,
|
||||
hasMore,
|
||||
|
||||
@@ -36,7 +36,7 @@ const JoinGroupScreen: React.FC = () => {
|
||||
|
||||
// 使用游标分页 Hook 管理群组列表
|
||||
const {
|
||||
items: groups,
|
||||
list: groups,
|
||||
isLoading,
|
||||
isRefreshing,
|
||||
hasMore,
|
||||
|
||||
@@ -164,7 +164,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
|
||||
// 【游标分页】使用 useCursorPagination hook 获取会话列表
|
||||
const {
|
||||
items: conversations,
|
||||
list: conversationList,
|
||||
isLoading,
|
||||
isRefreshing,
|
||||
hasMore,
|
||||
@@ -453,7 +453,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
if (activeSearchTab === 'chat') {
|
||||
const results: SearchResultItem[] = [];
|
||||
|
||||
for (const conv of conversations) {
|
||||
for (const conv of conversationList) {
|
||||
await messageManager.fetchMessages(String(conv.id));
|
||||
const messages = messageManager.getMessages(String(conv.id));
|
||||
const matchedMessages = messages.filter(msg => {
|
||||
@@ -484,7 +484,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
}, [conversations, activeSearchTab]);
|
||||
}, [conversationList, activeSearchTab]);
|
||||
|
||||
// 搜索文本变化时触发搜索
|
||||
useEffect(() => {
|
||||
@@ -533,7 +533,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
// 合并列表数据
|
||||
const listData: ConversationResponse[] = [
|
||||
createSystemMessageItem(),
|
||||
...conversations,
|
||||
...conversationList,
|
||||
];
|
||||
|
||||
// 总未读数
|
||||
@@ -923,7 +923,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{isLoading && conversations.length === 0 ? (
|
||||
{isLoading && conversationList.length === 0 ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||
</View>
|
||||
|
||||
@@ -88,7 +88,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
);
|
||||
|
||||
const {
|
||||
items: messages,
|
||||
list: messages,
|
||||
isLoading,
|
||||
isRefreshing,
|
||||
hasMore,
|
||||
|
||||
@@ -255,15 +255,18 @@ class CommentService {
|
||||
params: CursorPaginationRequest = {}
|
||||
): Promise<CursorPaginationResponse<Comment>> {
|
||||
try {
|
||||
const response = await api.get<CursorPaginationResponse<Comment>>(
|
||||
`/comments/post/${postId}/cursor`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
const response = await api.get<any>(`/comments/post/${postId}/cursor`, { params });
|
||||
const raw = response.data;
|
||||
return {
|
||||
list: Array.isArray(raw?.list) ? raw.list : [],
|
||||
next_cursor: raw?.next_cursor ?? null,
|
||||
prev_cursor: raw?.prev_cursor ?? null,
|
||||
has_more: raw?.has_more ?? false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取帖子评论列表失败:', error);
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
@@ -282,15 +285,18 @@ class CommentService {
|
||||
params: CursorPaginationRequest = {}
|
||||
): Promise<CursorPaginationResponse<Comment>> {
|
||||
try {
|
||||
const response = await api.get<CursorPaginationResponse<Comment>>(
|
||||
`/comments/${commentId}/replies/cursor`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
const response = await api.get<any>(`/comments/${commentId}/replies/cursor`, { params });
|
||||
const raw = response.data;
|
||||
return {
|
||||
list: Array.isArray(raw?.list) ? raw.list : [],
|
||||
next_cursor: raw?.next_cursor ?? null,
|
||||
prev_cursor: raw?.prev_cursor ?? null,
|
||||
has_more: raw?.has_more ?? false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取评论回复列表失败:', error);
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
|
||||
@@ -341,7 +341,7 @@ class GroupService {
|
||||
} catch (error) {
|
||||
console.error('获取群组列表失败:', error);
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
@@ -368,7 +368,7 @@ class GroupService {
|
||||
} catch (error) {
|
||||
console.error('获取群组成员列表失败:', error);
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
@@ -395,7 +395,7 @@ class GroupService {
|
||||
} catch (error) {
|
||||
console.error('获取群公告列表失败:', error);
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
|
||||
@@ -573,11 +573,17 @@ class MessageService {
|
||||
'/conversations/cursor',
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
const raw = response.data;
|
||||
return {
|
||||
list: Array.isArray(raw?.list) ? raw.list : [],
|
||||
next_cursor: raw?.next_cursor ?? null,
|
||||
prev_cursor: raw?.prev_cursor ?? null,
|
||||
has_more: raw?.has_more ?? false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取会话列表失败:', error);
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
@@ -596,15 +602,21 @@ class MessageService {
|
||||
params: CursorPaginationRequest = {}
|
||||
): Promise<CursorPaginationResponse<MessageResponse>> {
|
||||
try {
|
||||
const response = await api.get<CursorPaginationResponse<MessageResponse>>(
|
||||
const response = await api.get<any>(
|
||||
`/conversations/${encodeURIComponent(conversationId)}/messages/cursor`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
const raw = response.data;
|
||||
return {
|
||||
list: Array.isArray(raw?.list) ? raw.list : [],
|
||||
next_cursor: raw?.next_cursor ?? null,
|
||||
prev_cursor: raw?.prev_cursor ?? null,
|
||||
has_more: raw?.has_more ?? false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取消息列表失败:', error);
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
@@ -631,32 +643,36 @@ class MessageService {
|
||||
|
||||
const data = response.data;
|
||||
|
||||
// 兼容两种 API 响应格式
|
||||
// 游标或带 page/total_pages 的分页(统一为 CursorPaginationResponse:list)
|
||||
if (data && typeof data === 'object') {
|
||||
// 游标分页格式
|
||||
if (Array.isArray(data.items)) {
|
||||
const list = Array.isArray(data.list) ? data.list : null;
|
||||
if (list) {
|
||||
const isPageShape =
|
||||
typeof data.page === 'number' &&
|
||||
typeof data.total_pages === 'number' &&
|
||||
data.next_cursor === undefined &&
|
||||
data.prev_cursor === undefined;
|
||||
if (isPageShape) {
|
||||
const currentPage = data.page || 1;
|
||||
const totalPages = data.total_pages || 1;
|
||||
return {
|
||||
list,
|
||||
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
|
||||
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
|
||||
has_more: currentPage < totalPages,
|
||||
};
|
||||
}
|
||||
return {
|
||||
items: data.items,
|
||||
list,
|
||||
next_cursor: data.next_cursor ?? null,
|
||||
prev_cursor: data.prev_cursor ?? null,
|
||||
has_more: data.has_more ?? false,
|
||||
};
|
||||
}
|
||||
// 传统分页格式 - 转换为游标格式
|
||||
if (Array.isArray(data.list)) {
|
||||
const currentPage = data.page || 1;
|
||||
const totalPages = data.total_pages || 1;
|
||||
return {
|
||||
items: data.list,
|
||||
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
|
||||
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
|
||||
has_more: currentPage < totalPages,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
@@ -664,7 +680,7 @@ class MessageService {
|
||||
} catch (error) {
|
||||
console.error('获取系统消息列表失败:', error);
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
|
||||
@@ -172,7 +172,7 @@ class NotificationService {
|
||||
} catch (error) {
|
||||
console.error('获取通知列表失败:', error);
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
|
||||
@@ -305,36 +305,32 @@ class PostService {
|
||||
|
||||
const data = response.data;
|
||||
|
||||
// 兼容两种 API 响应格式:
|
||||
// 1. 游标分页格式:{ items, next_cursor, prev_cursor, has_more }
|
||||
// 2. 传统分页格式:{ list, total, page, page_size, total_pages }
|
||||
if (data && typeof data === 'object') {
|
||||
// 游标分页格式
|
||||
if (Array.isArray(data.items)) {
|
||||
return {
|
||||
items: data.items,
|
||||
next_cursor: data.next_cursor ?? null,
|
||||
prev_cursor: data.prev_cursor ?? null,
|
||||
has_more: data.has_more ?? false,
|
||||
};
|
||||
}
|
||||
// 传统分页格式 - 转换为游标格式
|
||||
if (Array.isArray(data.list)) {
|
||||
const currentPage = data.page || 1;
|
||||
const totalPages = data.total_pages || 1;
|
||||
return {
|
||||
items: data.list,
|
||||
// 使用页码作为游标
|
||||
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
|
||||
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
|
||||
has_more: currentPage < totalPages,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (data && typeof data === 'object' && Array.isArray(data.list)) {
|
||||
const isPageShape =
|
||||
typeof data.page === 'number' &&
|
||||
typeof data.total_pages === 'number' &&
|
||||
data.next_cursor === undefined &&
|
||||
data.prev_cursor === undefined;
|
||||
if (isPageShape) {
|
||||
const currentPage = data.page || 1;
|
||||
const totalPages = data.total_pages || 1;
|
||||
return {
|
||||
list: data.list,
|
||||
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
|
||||
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
|
||||
has_more: currentPage < totalPages,
|
||||
};
|
||||
}
|
||||
return {
|
||||
list: data.list,
|
||||
next_cursor: data.next_cursor ?? null,
|
||||
prev_cursor: data.prev_cursor ?? null,
|
||||
has_more: data.has_more ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
// 默认返回空数据
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
@@ -342,7 +338,7 @@ class PostService {
|
||||
} catch (error) {
|
||||
console.error('获取帖子列表失败:', error);
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
@@ -368,30 +364,32 @@ class PostService {
|
||||
|
||||
const data = response.data;
|
||||
|
||||
// 兼容两种 API 响应格式
|
||||
if (data && typeof data === 'object') {
|
||||
if (Array.isArray(data.items)) {
|
||||
return {
|
||||
items: data.items,
|
||||
next_cursor: data.next_cursor ?? null,
|
||||
prev_cursor: data.prev_cursor ?? null,
|
||||
has_more: data.has_more ?? false,
|
||||
};
|
||||
}
|
||||
if (Array.isArray(data.list)) {
|
||||
if (data && typeof data === 'object' && Array.isArray(data.list)) {
|
||||
const isPageShape =
|
||||
typeof data.page === 'number' &&
|
||||
typeof data.total_pages === 'number' &&
|
||||
data.next_cursor === undefined &&
|
||||
data.prev_cursor === undefined;
|
||||
if (isPageShape) {
|
||||
const currentPage = data.page || 1;
|
||||
const totalPages = data.total_pages || 1;
|
||||
return {
|
||||
items: data.list,
|
||||
list: data.list,
|
||||
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
|
||||
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
|
||||
has_more: currentPage < totalPages,
|
||||
};
|
||||
}
|
||||
return {
|
||||
list: data.list,
|
||||
next_cursor: data.next_cursor ?? null,
|
||||
prev_cursor: data.prev_cursor ?? null,
|
||||
has_more: data.has_more ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
@@ -399,7 +397,7 @@ class PostService {
|
||||
} catch (error) {
|
||||
console.error('搜索帖子失败:', error);
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
@@ -425,30 +423,32 @@ class PostService {
|
||||
|
||||
const data = response.data;
|
||||
|
||||
// 兼容两种 API 响应格式
|
||||
if (data && typeof data === 'object') {
|
||||
if (Array.isArray(data.items)) {
|
||||
return {
|
||||
items: data.items,
|
||||
next_cursor: data.next_cursor ?? null,
|
||||
prev_cursor: data.prev_cursor ?? null,
|
||||
has_more: data.has_more ?? false,
|
||||
};
|
||||
}
|
||||
if (Array.isArray(data.list)) {
|
||||
if (data && typeof data === 'object' && Array.isArray(data.list)) {
|
||||
const isPageShape =
|
||||
typeof data.page === 'number' &&
|
||||
typeof data.total_pages === 'number' &&
|
||||
data.next_cursor === undefined &&
|
||||
data.prev_cursor === undefined;
|
||||
if (isPageShape) {
|
||||
const currentPage = data.page || 1;
|
||||
const totalPages = data.total_pages || 1;
|
||||
return {
|
||||
items: data.list,
|
||||
list: data.list,
|
||||
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
|
||||
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
|
||||
has_more: currentPage < totalPages,
|
||||
};
|
||||
}
|
||||
return {
|
||||
list: data.list,
|
||||
next_cursor: data.next_cursor ?? null,
|
||||
prev_cursor: data.prev_cursor ?? null,
|
||||
has_more: data.has_more ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
@@ -456,7 +456,7 @@ class PostService {
|
||||
} catch (error) {
|
||||
console.error('获取用户帖子列表失败:', error);
|
||||
return {
|
||||
items: [],
|
||||
list: [],
|
||||
next_cursor: null,
|
||||
prev_cursor: null,
|
||||
has_more: false,
|
||||
|
||||
@@ -489,7 +489,7 @@ export interface CursorPaginationRequest {
|
||||
*/
|
||||
export interface CursorPaginationResponse<T> {
|
||||
/** 数据项列表 */
|
||||
items: T[];
|
||||
list: T[];
|
||||
/** 下一页游标 */
|
||||
next_cursor: string | null;
|
||||
/** 上一页游标 */
|
||||
|
||||
Reference in New Issue
Block a user