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 }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
provenance: false
|
provenance: false
|
||||||
cache-from: type=gha
|
cache-from: type=registry,ref=code.littlelan.cn/carrot_bbs/frontend-web:buildcache
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=registry,ref=code.littlelan.cn/carrot_bbs/frontend-web:buildcache,mode=max
|
||||||
|
|
||||||
- name: Show image tags
|
- name: Show image tags
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -64,10 +64,14 @@ export interface PostsState {
|
|||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
/** 错误信息 */
|
/** 错误信息 */
|
||||||
error: string | null;
|
error: string | null;
|
||||||
/** 当前游标 */
|
/** 当前游标(cursor 模式) */
|
||||||
cursor: string | null;
|
cursor: string | null;
|
||||||
|
/** 当前页码(page 模式) */
|
||||||
|
currentPage: number;
|
||||||
/** 最后加载时间 */
|
/** 最后加载时间 */
|
||||||
lastLoadTime: number | null;
|
lastLoadTime: number | null;
|
||||||
|
/** 上次请求的参数(用于加载更多和刷新) */
|
||||||
|
lastParams?: GetPostsParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -198,6 +202,7 @@ class ProcessPostUseCase {
|
|||||||
hasMore: true,
|
hasMore: true,
|
||||||
error: null,
|
error: null,
|
||||||
cursor: null,
|
cursor: null,
|
||||||
|
currentPage: 1,
|
||||||
lastLoadTime: null,
|
lastLoadTime: null,
|
||||||
};
|
};
|
||||||
this.postsState.set(key, state);
|
this.postsState.set(key, state);
|
||||||
@@ -257,29 +262,32 @@ class ProcessPostUseCase {
|
|||||||
* @param key 列表键(用于区分不同的列表)
|
* @param key 列表键(用于区分不同的列表)
|
||||||
*/
|
*/
|
||||||
async fetchPosts(params?: GetPostsParams, key: string = 'default'): Promise<PostsResult> {
|
async fetchPosts(params?: GetPostsParams, key: string = 'default'): Promise<PostsResult> {
|
||||||
const state = this.getPostsState(key);
|
|
||||||
|
|
||||||
// 更新加载状态
|
// 更新加载状态
|
||||||
this.updatePostsState(key, { isLoading: true, error: null });
|
this.updatePostsState(key, { isLoading: true, error: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 合并分页参数
|
// 默认传递空字符串 cursor 来启动 cursor 模式
|
||||||
|
// 注意:cursor 要放在 ...params 后面,这样只有在 params 没有传 cursor 时才使用默认值
|
||||||
const queryParams: GetPostsParams = {
|
const queryParams: GetPostsParams = {
|
||||||
page: params?.page || 1,
|
|
||||||
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
|
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
|
||||||
cursor: params?.cursor || state.cursor || undefined,
|
|
||||||
...params,
|
...params,
|
||||||
|
cursor: params?.cursor !== undefined ? params.cursor : '', // 默认使用空字符串启动 cursor 模式
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await postRepository.getPosts(queryParams);
|
const result = await postRepository.getPosts(queryParams);
|
||||||
|
|
||||||
// 更新状态
|
// 判断是否为 cursor 模式(有 next_cursor 返回)
|
||||||
|
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
||||||
|
|
||||||
|
// 更新状态(保存请求参数以便加载更多时使用)
|
||||||
this.updatePostsState(key, {
|
this.updatePostsState(key, {
|
||||||
posts: result.posts,
|
posts: result.posts,
|
||||||
hasMore: result.hasMore,
|
hasMore: result.hasMore,
|
||||||
cursor: result.nextCursor || null,
|
cursor: isCursorMode ? result.nextCursor : null, // cursor 模式保存 cursor,否则设为 null 表示 page 模式
|
||||||
|
currentPage: isCursorMode ? undefined : 1, // page 模式从第 1 页开始
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
lastLoadTime: Date.now(),
|
lastLoadTime: Date.now(),
|
||||||
|
lastParams: params, // 保存请求参数
|
||||||
});
|
});
|
||||||
|
|
||||||
// 通知订阅者
|
// 通知订阅者
|
||||||
@@ -312,20 +320,29 @@ class ProcessPostUseCase {
|
|||||||
* @param key 列表键
|
* @param key 列表键
|
||||||
*/
|
*/
|
||||||
async refreshPosts(key: string = 'default'): Promise<PostsResult> {
|
async refreshPosts(key: string = 'default'): Promise<PostsResult> {
|
||||||
|
const state = this.getPostsState(key);
|
||||||
|
|
||||||
// 更新刷新状态
|
// 更新刷新状态
|
||||||
this.updatePostsState(key, { isRefreshing: true, error: null });
|
this.updatePostsState(key, { isRefreshing: true, error: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 使用保存的请求参数(如 post_type)进行刷新
|
||||||
|
// 默认传递空字符串 cursor 来启动 cursor 模式
|
||||||
const result = await postRepository.getPosts({
|
const result = await postRepository.getPosts({
|
||||||
page: 1,
|
|
||||||
pageSize: DEFAULT_PAGE_SIZE,
|
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, {
|
this.updatePostsState(key, {
|
||||||
posts: result.posts,
|
posts: result.posts,
|
||||||
hasMore: result.hasMore,
|
hasMore: result.hasMore,
|
||||||
cursor: result.nextCursor || null,
|
cursor: isCursorMode ? result.nextCursor : null,
|
||||||
|
currentPage: isCursorMode ? undefined : 1,
|
||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
lastLoadTime: Date.now(),
|
lastLoadTime: Date.now(),
|
||||||
});
|
});
|
||||||
@@ -372,21 +389,45 @@ class ProcessPostUseCase {
|
|||||||
this.updatePostsState(key, { isLoading: true, error: null });
|
this.updatePostsState(key, { isLoading: true, error: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const queryParams: GetPostsParams = {
|
// 判断使用哪种分页模式:cursor 不为 null 表示使用 cursor 模式
|
||||||
pageSize: DEFAULT_PAGE_SIZE,
|
const useCursorMode = state.cursor !== null && state.cursor !== undefined;
|
||||||
cursor: 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 result = await postRepository.getPosts(queryParams);
|
||||||
|
|
||||||
// 合并新数据
|
// 合并新数据
|
||||||
const newPosts = [...state.posts, ...result.posts];
|
const newPosts = [...state.posts, ...result.posts];
|
||||||
|
|
||||||
|
// 判断响应是否为 cursor 模式(有 next_cursor 返回)
|
||||||
|
const responseIsCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
||||||
|
|
||||||
// 更新状态
|
// 更新状态
|
||||||
this.updatePostsState(key, {
|
this.updatePostsState(key, {
|
||||||
posts: newPosts,
|
posts: newPosts,
|
||||||
hasMore: result.hasMore,
|
hasMore: result.hasMore,
|
||||||
cursor: result.nextCursor || null,
|
cursor: responseIsCursorMode ? result.nextCursor : null,
|
||||||
|
currentPage: useCursorMode ? state.currentPage : ((state.currentPage || 1) + 1),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
lastLoadTime: Date.now(),
|
lastLoadTime: Date.now(),
|
||||||
});
|
});
|
||||||
@@ -398,6 +439,7 @@ class ProcessPostUseCase {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : '加载更多帖子失败';
|
const errorMessage = error instanceof Error ? error.message : '加载更多帖子失败';
|
||||||
|
console.error('[ProcessPostUseCase] Load more error:', errorMessage);
|
||||||
this.updatePostsState(key, {
|
this.updatePostsState(key, {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
|
|||||||
@@ -231,9 +231,12 @@ export class PostRepository implements IPostRepository {
|
|||||||
try {
|
try {
|
||||||
const queryParams: Record<string, any> = {};
|
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?.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?.post_type) queryParams.tab = params.post_type;
|
||||||
if (params?.communityId) queryParams.community_id = params.communityId;
|
if (params?.communityId) queryParams.community_id = params.communityId;
|
||||||
if (params?.authorId) queryParams.author_id = params.authorId;
|
if (params?.authorId) queryParams.author_id = params.authorId;
|
||||||
@@ -253,9 +256,16 @@ export class PostRepository implements IPostRepository {
|
|||||||
this.saveToLocalCache(post);
|
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 {
|
return {
|
||||||
posts,
|
posts,
|
||||||
hasMore: response.has_more ?? false,
|
hasMore,
|
||||||
nextCursor: response.next_cursor,
|
nextCursor: response.next_cursor,
|
||||||
total: response.total,
|
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 effectivePageSize = Math.min(Math.max(1, pageSize), MAX_PAGE_SIZE);
|
||||||
|
|
||||||
const [state, setState] = useState<CursorPaginationState<T>>({
|
const [state, setState] = useState<CursorPaginationState<T>>({
|
||||||
items: [],
|
list: [],
|
||||||
nextCursor: null,
|
nextCursor: null,
|
||||||
prevCursor: null,
|
prevCursor: null,
|
||||||
hasMore: true, // 初始设为 true,以便首次加载可以执行
|
hasMore: true, // 初始设为 true,以便首次加载可以执行
|
||||||
@@ -86,7 +86,7 @@ export function useCursorPagination<T, P = void>(
|
|||||||
|
|
||||||
setState(prev => ({
|
setState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
items: [...prev.items, ...response.items],
|
list: [...prev.list, ...response.list],
|
||||||
nextCursor: response.next_cursor,
|
nextCursor: response.next_cursor,
|
||||||
prevCursor: response.prev_cursor,
|
prevCursor: response.prev_cursor,
|
||||||
hasMore: response.has_more && response.next_cursor !== null,
|
hasMore: response.has_more && response.next_cursor !== null,
|
||||||
@@ -134,7 +134,7 @@ export function useCursorPagination<T, P = void>(
|
|||||||
setState(prev => ({
|
setState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
// 向前加载时,新数据放在前面
|
// 向前加载时,新数据放在前面
|
||||||
items: [...response.items, ...prev.items],
|
list: [...response.list, ...prev.list],
|
||||||
nextCursor: response.next_cursor,
|
nextCursor: response.next_cursor,
|
||||||
prevCursor: response.prev_cursor,
|
prevCursor: response.prev_cursor,
|
||||||
hasMore: response.has_more,
|
hasMore: response.has_more,
|
||||||
@@ -169,7 +169,7 @@ export function useCursorPagination<T, P = void>(
|
|||||||
if (cancelledRef.current) return;
|
if (cancelledRef.current) return;
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
items: response.items,
|
list: response.list,
|
||||||
nextCursor: response.next_cursor,
|
nextCursor: response.next_cursor,
|
||||||
prevCursor: response.prev_cursor,
|
prevCursor: response.prev_cursor,
|
||||||
hasMore: response.has_more && response.next_cursor !== null,
|
hasMore: response.has_more && response.next_cursor !== null,
|
||||||
@@ -196,7 +196,7 @@ export function useCursorPagination<T, P = void>(
|
|||||||
isFirstLoadRef.current = true;
|
isFirstLoadRef.current = true;
|
||||||
isLoadingRef.current = false;
|
isLoadingRef.current = false;
|
||||||
setState({
|
setState({
|
||||||
items: [],
|
list: [],
|
||||||
nextCursor: null,
|
nextCursor: null,
|
||||||
prevCursor: null,
|
prevCursor: null,
|
||||||
hasMore: true, // 重置后也设为 true,以便可以重新加载
|
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,
|
nextCursor: string | null,
|
||||||
prevCursor: string | null,
|
prevCursor: string | null,
|
||||||
hasMore: boolean
|
hasMore: boolean
|
||||||
) => {
|
) => {
|
||||||
setState(prev => ({
|
setState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
items,
|
list,
|
||||||
nextCursor,
|
nextCursor,
|
||||||
prevCursor,
|
prevCursor,
|
||||||
hasMore,
|
hasMore,
|
||||||
@@ -230,7 +230,7 @@ export function useCursorPagination<T, P = void>(
|
|||||||
// 自动加载第一页
|
// 自动加载第一页
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 使用 ref 防止重复加载,不依赖 loadMore 函数引用
|
// 使用 ref 防止重复加载,不依赖 loadMore 函数引用
|
||||||
// 只检查 autoLoad 和 autoLoadRef,不再依赖 state.items.length
|
// 只检查 autoLoad 和 autoLoadRef,不再依赖 state.list.length
|
||||||
if (!autoLoad || autoLoadRef.current) return;
|
if (!autoLoad || autoLoadRef.current) return;
|
||||||
|
|
||||||
autoLoadRef.current = true;
|
autoLoadRef.current = true;
|
||||||
@@ -261,7 +261,7 @@ export function useCursorPagination<T, P = void>(
|
|||||||
|
|
||||||
setState(prev => ({
|
setState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
items: response.items,
|
list: response.list,
|
||||||
nextCursor: response.next_cursor,
|
nextCursor: response.next_cursor,
|
||||||
prevCursor: response.prev_cursor,
|
prevCursor: response.prev_cursor,
|
||||||
hasMore: response.has_more && response.next_cursor !== null,
|
hasMore: response.has_more && response.next_cursor !== null,
|
||||||
@@ -296,7 +296,7 @@ export function useCursorPagination<T, P = void>(
|
|||||||
loadPrevious,
|
loadPrevious,
|
||||||
refresh,
|
refresh,
|
||||||
reset,
|
reset,
|
||||||
setItems,
|
setList,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,12 @@
|
|||||||
* 集成 ProcessPostUseCase 进行状态管理
|
* 集成 ProcessPostUseCase 进行状态管理
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
PostIdentifier,
|
PostIdentifier,
|
||||||
PostUpdate,
|
PostUpdate,
|
||||||
PostUpdateType,
|
PostUpdateType,
|
||||||
PostDiffConfig,
|
PostDiffConfig,
|
||||||
PostDiffResult,
|
|
||||||
} from '../infrastructure/diff/postTypes';
|
} from '../infrastructure/diff/postTypes';
|
||||||
import {
|
import {
|
||||||
PostDiffCalculator,
|
PostDiffCalculator,
|
||||||
@@ -119,7 +118,6 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
|||||||
enableDiff = true,
|
enableDiff = true,
|
||||||
enableBatching = true,
|
enableBatching = true,
|
||||||
maxPostCount = 10000,
|
maxPostCount = 10000,
|
||||||
changeRatioThreshold = 0.5,
|
|
||||||
listKey = 'default',
|
listKey = 'default',
|
||||||
autoSubscribe = true,
|
autoSubscribe = true,
|
||||||
} = options;
|
} = options;
|
||||||
@@ -147,37 +145,7 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
|||||||
const previousPostsRef = useRef<T[]>(initialPosts);
|
const previousPostsRef = useRef<T[]>(initialPosts);
|
||||||
const unsubscribeRef = useRef<(() => void) | null>(null);
|
const unsubscribeRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
// ==================== 初始化 ====================
|
// ==================== 批量更新处理(须在订阅 batcher 的 useEffect 之前定义)====================
|
||||||
|
|
||||||
// 初始化差异计算器
|
|
||||||
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]);
|
|
||||||
|
|
||||||
// ==================== 批量更新处理 ====================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理批量更新
|
* 处理批量更新
|
||||||
@@ -289,123 +257,34 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ==================== 差异计算 ====================
|
// ==================== 初始化 ====================
|
||||||
|
|
||||||
/**
|
// 初始化差异计算器
|
||||||
* 计算并应用差异
|
useEffect(() => {
|
||||||
*/
|
if (enableDiff) {
|
||||||
const calculateAndApplyDiff = useCallback((newPosts: T[]) => {
|
calculatorRef.current = createPostDiffCalculator<T>(diffConfig);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
return () => {
|
||||||
|
calculatorRef.current = null;
|
||||||
|
};
|
||||||
|
}, [enableDiff, diffConfig]);
|
||||||
|
|
||||||
// 使用差异计算
|
// 初始化批量处理器
|
||||||
if (enableDiff && calculatorRef.current) {
|
useEffect(() => {
|
||||||
const diff = calculatorRef.current.calculateDiff(previousPosts, newPosts);
|
if (!enableBatching) return;
|
||||||
|
|
||||||
if (diff.shouldReset) {
|
batcherRef.current = createPostUpdateBatcher(batcherOptions);
|
||||||
setPosts(newPosts);
|
|
||||||
} else {
|
|
||||||
// 将差异转换为更新操作
|
|
||||||
const updates: PostUpdate[] = [];
|
|
||||||
|
|
||||||
// 处理新增
|
const unsubscribe = batcherRef.current.subscribe((updates) => {
|
||||||
if (diff.added.length > 0) {
|
handleBatchUpdates(updates);
|
||||||
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(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理更新
|
return () => {
|
||||||
if (diff.updated.length > 0) {
|
unsubscribe();
|
||||||
if (diff.updated.length === 1) {
|
batcherRef.current?.destroy();
|
||||||
updates.push({
|
batcherRef.current = null;
|
||||||
type: PostUpdateType.UPDATE,
|
};
|
||||||
postId: diff.updated[0].post.id,
|
}, [enableBatching, batcherOptions, handleBatchUpdates]);
|
||||||
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]);
|
|
||||||
|
|
||||||
// ==================== UseCase 订阅 ====================
|
// ==================== UseCase 订阅 ====================
|
||||||
|
|
||||||
@@ -415,26 +294,39 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
|||||||
const handleUseCaseEvent = useCallback((event: PostUseCaseEvent) => {
|
const handleUseCaseEvent = useCallback((event: PostUseCaseEvent) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'state_changed': {
|
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) {
|
if (state) {
|
||||||
setLoading(state.isLoading);
|
setLoading(state.isLoading);
|
||||||
setRefreshing(state.isRefreshing);
|
setRefreshing(state.isRefreshing);
|
||||||
setError(state.error);
|
setError(state.error);
|
||||||
setHasMore(state.hasMore);
|
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) {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'posts_loaded': {
|
case 'posts_loaded': {
|
||||||
const { posts: loadedPosts } = event.payload;
|
// fetchPosts 在 updatePostsState 时已发出 state_changed(含合并后 posts),此处不再应用,避免与其它 key 竞态或重复计算
|
||||||
if (loadedPosts) {
|
|
||||||
calculateAndApplyDiff(loadedPosts as T[]);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,12 +377,17 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'error_occurred': {
|
case 'error_occurred': {
|
||||||
const { error: errorMsg } = event.payload;
|
const payload = event.payload as { key?: string; error?: string };
|
||||||
setError(errorMsg);
|
if (payload.key !== undefined && payload.key !== listKey) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (payload.error !== undefined) {
|
||||||
|
setError(payload.error);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [calculateAndApplyDiff]);
|
}, [listKey, maxPostCount]);
|
||||||
|
|
||||||
// 订阅 UseCase 状态变化
|
// 订阅 UseCase 状态变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export interface CursorPaginationConfig {
|
|||||||
*/
|
*/
|
||||||
export interface CursorPaginationState<T> {
|
export interface CursorPaginationState<T> {
|
||||||
/** 数据项列表 */
|
/** 数据项列表 */
|
||||||
items: T[];
|
list: T[];
|
||||||
/** 下一页游标 */
|
/** 下一页游标 */
|
||||||
nextCursor: string | null;
|
nextCursor: string | null;
|
||||||
/** 上一页游标 */
|
/** 上一页游标 */
|
||||||
@@ -235,7 +235,7 @@ export interface CursorPaginationActions<T> {
|
|||||||
/** 重置状态 */
|
/** 重置状态 */
|
||||||
reset: () => void;
|
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;
|
extraParams?: P;
|
||||||
}) => Promise<{
|
}) => Promise<{
|
||||||
items: T[];
|
list: T[];
|
||||||
next_cursor: string | null;
|
next_cursor: string | null;
|
||||||
prev_cursor: string | null;
|
prev_cursor: string | null;
|
||||||
has_more: boolean;
|
has_more: boolean;
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ export const HomeScreen: React.FC = () => {
|
|||||||
// 用于跟踪当前页面显示的帖子 ID,以便从 store 同步状态
|
// 用于跟踪当前页面显示的帖子 ID,以便从 store 同步状态
|
||||||
const postIdsRef = useRef<Set<string>>(new Set());
|
const postIdsRef = useRef<Set<string>>(new Set());
|
||||||
const lastSwipeAtRef = useRef(0);
|
const lastSwipeAtRef = useRef(0);
|
||||||
|
|
||||||
|
// 网格模式滚动位置检测
|
||||||
|
const gridScrollYRef = useRef(0);
|
||||||
|
const isLoadingMoreRef = useRef(false);
|
||||||
|
|
||||||
// 构建一个以 postId 为 key 的 map,用于快速查找
|
// 构建一个以 postId 为 key 的 map,用于快速查找
|
||||||
const postsMap = useMemo(() => {
|
const postsMap = useMemo(() => {
|
||||||
@@ -131,14 +135,30 @@ export const HomeScreen: React.FC = () => {
|
|||||||
|
|
||||||
// 加载更多方法
|
// 加载更多方法
|
||||||
const loadMore = useCallback(async () => {
|
const loadMore = useCallback(async () => {
|
||||||
if (hasMore && !isLoading) {
|
if (hasMore && !isLoading && !isLoadingMoreRef.current) {
|
||||||
|
isLoadingMoreRef.current = true;
|
||||||
try {
|
try {
|
||||||
await processPostUseCase.loadMorePosts(listKey);
|
await processPostUseCase.loadMorePosts(listKey);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载更多失败:', err);
|
console.error('加载更多失败:', err);
|
||||||
|
} finally {
|
||||||
|
isLoadingMoreRef.current = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [listKey, hasMore, isLoading]);
|
}, [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 () => {
|
const refresh = useCallback(async () => {
|
||||||
@@ -485,6 +505,7 @@ export const HomeScreen: React.FC = () => {
|
|||||||
]}
|
]}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
scrollEventThrottle={100}
|
scrollEventThrottle={100}
|
||||||
|
onScroll={handleGridScroll}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={isRefreshing}
|
refreshing={isRefreshing}
|
||||||
@@ -495,6 +516,12 @@ export const HomeScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{distributePostsToColumns.map((column, index) => renderWaterfallColumn(column, index))}
|
{distributePostsToColumns.map((column, index) => renderWaterfallColumn(column, index))}
|
||||||
|
{/* 加载更多指示器 */}
|
||||||
|
{isLoading && displayPosts.length > 0 && (
|
||||||
|
<View style={styles.loadingMore}>
|
||||||
|
<Loading size="sm" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -767,4 +794,9 @@ const styles = StyleSheet.create({
|
|||||||
height: 72,
|
height: 72,
|
||||||
borderRadius: 36,
|
borderRadius: 36,
|
||||||
},
|
},
|
||||||
|
loadingMore: {
|
||||||
|
paddingVertical: 20,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
|
|
||||||
// 使用游标分页 Hook 管理评论列表
|
// 使用游标分页 Hook 管理评论列表
|
||||||
const {
|
const {
|
||||||
items: paginatedComments,
|
list: paginatedComments,
|
||||||
isLoading: isCommentsLoading,
|
isLoading: isCommentsLoading,
|
||||||
isRefreshing: isCommentsRefreshing,
|
isRefreshing: isCommentsRefreshing,
|
||||||
hasMore: hasMoreComments,
|
hasMore: hasMoreComments,
|
||||||
@@ -206,13 +206,13 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
|
|
||||||
// 如果是从评论按钮跳转过来的,加载完成后滚动到评论区
|
// 如果是从评论按钮跳转过来的,加载完成后滚动到评论区
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldScrollToComments && !loading && comments.length > 0) {
|
if (shouldScrollToComments && !loading && (comments?.length ?? 0) > 0) {
|
||||||
// 延迟确保列表已完成渲染和布局
|
// 延迟确保列表已完成渲染和布局
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
flatListRef.current?.scrollToIndex({ index: 0, animated: true, viewPosition: 0 });
|
flatListRef.current?.scrollToIndex({ index: 0, animated: true, viewPosition: 0 });
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}, [shouldScrollToComments, loading, comments.length]);
|
}, [shouldScrollToComments, loading, comments?.length]);
|
||||||
|
|
||||||
// 动态设置导航头部
|
// 动态设置导航头部
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1413,7 +1413,7 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
加载更多评论
|
加载更多评论
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : comments.length > 0 ? (
|
) : (comments?.length ?? 0) > 0 ? (
|
||||||
<Text variant="caption" color={colors.text.hint} style={styles.noMoreComments}>
|
<Text variant="caption" color={colors.text.hint} style={styles.noMoreComments}>
|
||||||
没有更多评论了
|
没有更多评论了
|
||||||
</Text>
|
</Text>
|
||||||
@@ -1546,7 +1546,7 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
加载更多评论
|
加载更多评论
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : comments.length > 0 ? (
|
) : (comments?.length ?? 0) > 0 ? (
|
||||||
<Text variant="caption" color={colors.text.hint} style={styles.noMoreComments}>
|
<Text variant="caption" color={colors.text.hint} style={styles.noMoreComments}>
|
||||||
没有更多评论了
|
没有更多评论了
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
|||||||
|
|
||||||
// 使用游标分页进行帖子搜索
|
// 使用游标分页进行帖子搜索
|
||||||
const {
|
const {
|
||||||
items: searchResults,
|
list: searchResults,
|
||||||
isLoading,
|
isLoading,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
hasMore,
|
hasMore,
|
||||||
@@ -93,7 +93,7 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
|||||||
} = useCursorPagination<Post, { query: string }>(
|
} = useCursorPagination<Post, { query: string }>(
|
||||||
async ({ cursor, pageSize, extraParams }) => {
|
async ({ cursor, pageSize, extraParams }) => {
|
||||||
if (!extraParams?.query) {
|
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, {
|
const response = await postService.searchPostsCursor(extraParams.query, {
|
||||||
cursor,
|
cursor,
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const GroupMembersScreen: React.FC = () => {
|
|||||||
|
|
||||||
// 使用游标分页 Hook 管理成员列表
|
// 使用游标分页 Hook 管理成员列表
|
||||||
const {
|
const {
|
||||||
items: members,
|
list: members,
|
||||||
isLoading,
|
isLoading,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
hasMore,
|
hasMore,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const JoinGroupScreen: React.FC = () => {
|
|||||||
|
|
||||||
// 使用游标分页 Hook 管理群组列表
|
// 使用游标分页 Hook 管理群组列表
|
||||||
const {
|
const {
|
||||||
items: groups,
|
list: groups,
|
||||||
isLoading,
|
isLoading,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
hasMore,
|
hasMore,
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
|
|
||||||
// 【游标分页】使用 useCursorPagination hook 获取会话列表
|
// 【游标分页】使用 useCursorPagination hook 获取会话列表
|
||||||
const {
|
const {
|
||||||
items: conversations,
|
list: conversationList,
|
||||||
isLoading,
|
isLoading,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
hasMore,
|
hasMore,
|
||||||
@@ -453,7 +453,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
if (activeSearchTab === 'chat') {
|
if (activeSearchTab === 'chat') {
|
||||||
const results: SearchResultItem[] = [];
|
const results: SearchResultItem[] = [];
|
||||||
|
|
||||||
for (const conv of conversations) {
|
for (const conv of conversationList) {
|
||||||
await messageManager.fetchMessages(String(conv.id));
|
await messageManager.fetchMessages(String(conv.id));
|
||||||
const messages = messageManager.getMessages(String(conv.id));
|
const messages = messageManager.getMessages(String(conv.id));
|
||||||
const matchedMessages = messages.filter(msg => {
|
const matchedMessages = messages.filter(msg => {
|
||||||
@@ -484,7 +484,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsSearching(false);
|
setIsSearching(false);
|
||||||
}
|
}
|
||||||
}, [conversations, activeSearchTab]);
|
}, [conversationList, activeSearchTab]);
|
||||||
|
|
||||||
// 搜索文本变化时触发搜索
|
// 搜索文本变化时触发搜索
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -533,7 +533,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
// 合并列表数据
|
// 合并列表数据
|
||||||
const listData: ConversationResponse[] = [
|
const listData: ConversationResponse[] = [
|
||||||
createSystemMessageItem(),
|
createSystemMessageItem(),
|
||||||
...conversations,
|
...conversationList,
|
||||||
];
|
];
|
||||||
|
|
||||||
// 总未读数
|
// 总未读数
|
||||||
@@ -923,7 +923,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{isLoading && conversations.length === 0 ? (
|
{isLoading && conversationList.length === 0 ? (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
items: messages,
|
list: messages,
|
||||||
isLoading,
|
isLoading,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
hasMore,
|
hasMore,
|
||||||
|
|||||||
@@ -255,15 +255,18 @@ class CommentService {
|
|||||||
params: CursorPaginationRequest = {}
|
params: CursorPaginationRequest = {}
|
||||||
): Promise<CursorPaginationResponse<Comment>> {
|
): Promise<CursorPaginationResponse<Comment>> {
|
||||||
try {
|
try {
|
||||||
const response = await api.get<CursorPaginationResponse<Comment>>(
|
const response = await api.get<any>(`/comments/post/${postId}/cursor`, { params });
|
||||||
`/comments/post/${postId}/cursor`,
|
const raw = response.data;
|
||||||
{ params }
|
return {
|
||||||
);
|
list: Array.isArray(raw?.list) ? raw.list : [],
|
||||||
return response.data;
|
next_cursor: raw?.next_cursor ?? null,
|
||||||
|
prev_cursor: raw?.prev_cursor ?? null,
|
||||||
|
has_more: raw?.has_more ?? false,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取帖子评论列表失败:', error);
|
console.error('获取帖子评论列表失败:', error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
@@ -282,15 +285,18 @@ class CommentService {
|
|||||||
params: CursorPaginationRequest = {}
|
params: CursorPaginationRequest = {}
|
||||||
): Promise<CursorPaginationResponse<Comment>> {
|
): Promise<CursorPaginationResponse<Comment>> {
|
||||||
try {
|
try {
|
||||||
const response = await api.get<CursorPaginationResponse<Comment>>(
|
const response = await api.get<any>(`/comments/${commentId}/replies/cursor`, { params });
|
||||||
`/comments/${commentId}/replies/cursor`,
|
const raw = response.data;
|
||||||
{ params }
|
return {
|
||||||
);
|
list: Array.isArray(raw?.list) ? raw.list : [],
|
||||||
return response.data;
|
next_cursor: raw?.next_cursor ?? null,
|
||||||
|
prev_cursor: raw?.prev_cursor ?? null,
|
||||||
|
has_more: raw?.has_more ?? false,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取评论回复列表失败:', error);
|
console.error('获取评论回复列表失败:', error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ class GroupService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取群组列表失败:', error);
|
console.error('获取群组列表失败:', error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
@@ -368,7 +368,7 @@ class GroupService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取群组成员列表失败:', error);
|
console.error('获取群组成员列表失败:', error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
@@ -395,7 +395,7 @@ class GroupService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取群公告列表失败:', error);
|
console.error('获取群公告列表失败:', error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
|
|||||||
@@ -573,11 +573,17 @@ class MessageService {
|
|||||||
'/conversations/cursor',
|
'/conversations/cursor',
|
||||||
{ params }
|
{ 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) {
|
} catch (error) {
|
||||||
console.error('获取会话列表失败:', error);
|
console.error('获取会话列表失败:', error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
@@ -596,15 +602,21 @@ class MessageService {
|
|||||||
params: CursorPaginationRequest = {}
|
params: CursorPaginationRequest = {}
|
||||||
): Promise<CursorPaginationResponse<MessageResponse>> {
|
): Promise<CursorPaginationResponse<MessageResponse>> {
|
||||||
try {
|
try {
|
||||||
const response = await api.get<CursorPaginationResponse<MessageResponse>>(
|
const response = await api.get<any>(
|
||||||
`/conversations/${encodeURIComponent(conversationId)}/messages/cursor`,
|
`/conversations/${encodeURIComponent(conversationId)}/messages/cursor`,
|
||||||
{ params }
|
{ 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) {
|
} catch (error) {
|
||||||
console.error('获取消息列表失败:', error);
|
console.error('获取消息列表失败:', error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
@@ -631,32 +643,36 @@ class MessageService {
|
|||||||
|
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
// 兼容两种 API 响应格式
|
// 游标或带 page/total_pages 的分页(统一为 CursorPaginationResponse:list)
|
||||||
if (data && typeof data === 'object') {
|
if (data && typeof data === 'object') {
|
||||||
// 游标分页格式
|
const list = Array.isArray(data.list) ? data.list : null;
|
||||||
if (Array.isArray(data.items)) {
|
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 {
|
return {
|
||||||
items: data.items,
|
list,
|
||||||
next_cursor: data.next_cursor ?? null,
|
next_cursor: data.next_cursor ?? null,
|
||||||
prev_cursor: data.prev_cursor ?? null,
|
prev_cursor: data.prev_cursor ?? null,
|
||||||
has_more: data.has_more ?? false,
|
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 {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
@@ -664,7 +680,7 @@ class MessageService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取系统消息列表失败:', error);
|
console.error('获取系统消息列表失败:', error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ class NotificationService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取通知列表失败:', error);
|
console.error('获取通知列表失败:', error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
|
|||||||
@@ -304,37 +304,33 @@ class PostService {
|
|||||||
const response = await api.get<any>('/posts', requestParams);
|
const response = await api.get<any>('/posts', requestParams);
|
||||||
|
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
// 兼容两种 API 响应格式:
|
if (data && typeof data === 'object' && Array.isArray(data.list)) {
|
||||||
// 1. 游标分页格式:{ items, next_cursor, prev_cursor, has_more }
|
const isPageShape =
|
||||||
// 2. 传统分页格式:{ list, total, page, page_size, total_pages }
|
typeof data.page === 'number' &&
|
||||||
if (data && typeof data === 'object') {
|
typeof data.total_pages === 'number' &&
|
||||||
// 游标分页格式
|
data.next_cursor === undefined &&
|
||||||
if (Array.isArray(data.items)) {
|
data.prev_cursor === undefined;
|
||||||
return {
|
if (isPageShape) {
|
||||||
items: data.items,
|
const currentPage = data.page || 1;
|
||||||
next_cursor: data.next_cursor ?? null,
|
const totalPages = data.total_pages || 1;
|
||||||
prev_cursor: data.prev_cursor ?? null,
|
return {
|
||||||
has_more: data.has_more ?? false,
|
list: data.list,
|
||||||
};
|
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
|
||||||
}
|
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
|
||||||
// 传统分页格式 - 转换为游标格式
|
has_more: currentPage < totalPages,
|
||||||
if (Array.isArray(data.list)) {
|
};
|
||||||
const currentPage = data.page || 1;
|
}
|
||||||
const totalPages = data.total_pages || 1;
|
return {
|
||||||
return {
|
list: data.list,
|
||||||
items: data.list,
|
next_cursor: data.next_cursor ?? null,
|
||||||
// 使用页码作为游标
|
prev_cursor: data.prev_cursor ?? null,
|
||||||
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
|
has_more: data.has_more ?? false,
|
||||||
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
|
};
|
||||||
has_more: currentPage < totalPages,
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认返回空数据
|
|
||||||
return {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
@@ -342,7 +338,7 @@ class PostService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取帖子列表失败:', error);
|
console.error('获取帖子列表失败:', error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
@@ -367,31 +363,33 @@ class PostService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
// 兼容两种 API 响应格式
|
if (data && typeof data === 'object' && Array.isArray(data.list)) {
|
||||||
if (data && typeof data === 'object') {
|
const isPageShape =
|
||||||
if (Array.isArray(data.items)) {
|
typeof data.page === 'number' &&
|
||||||
return {
|
typeof data.total_pages === 'number' &&
|
||||||
items: data.items,
|
data.next_cursor === undefined &&
|
||||||
next_cursor: data.next_cursor ?? null,
|
data.prev_cursor === undefined;
|
||||||
prev_cursor: data.prev_cursor ?? null,
|
if (isPageShape) {
|
||||||
has_more: data.has_more ?? false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (Array.isArray(data.list)) {
|
|
||||||
const currentPage = data.page || 1;
|
const currentPage = data.page || 1;
|
||||||
const totalPages = data.total_pages || 1;
|
const totalPages = data.total_pages || 1;
|
||||||
return {
|
return {
|
||||||
items: data.list,
|
list: data.list,
|
||||||
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
|
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
|
||||||
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
|
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
|
||||||
has_more: currentPage < totalPages,
|
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 {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
@@ -399,7 +397,7 @@ class PostService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('搜索帖子失败:', error);
|
console.error('搜索帖子失败:', error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
@@ -424,31 +422,33 @@ class PostService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
// 兼容两种 API 响应格式
|
if (data && typeof data === 'object' && Array.isArray(data.list)) {
|
||||||
if (data && typeof data === 'object') {
|
const isPageShape =
|
||||||
if (Array.isArray(data.items)) {
|
typeof data.page === 'number' &&
|
||||||
return {
|
typeof data.total_pages === 'number' &&
|
||||||
items: data.items,
|
data.next_cursor === undefined &&
|
||||||
next_cursor: data.next_cursor ?? null,
|
data.prev_cursor === undefined;
|
||||||
prev_cursor: data.prev_cursor ?? null,
|
if (isPageShape) {
|
||||||
has_more: data.has_more ?? false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (Array.isArray(data.list)) {
|
|
||||||
const currentPage = data.page || 1;
|
const currentPage = data.page || 1;
|
||||||
const totalPages = data.total_pages || 1;
|
const totalPages = data.total_pages || 1;
|
||||||
return {
|
return {
|
||||||
items: data.list,
|
list: data.list,
|
||||||
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
|
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
|
||||||
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
|
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
|
||||||
has_more: currentPage < totalPages,
|
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 {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
@@ -456,7 +456,7 @@ class PostService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取用户帖子列表失败:', error);
|
console.error('获取用户帖子列表失败:', error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
list: [],
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
prev_cursor: null,
|
prev_cursor: null,
|
||||||
has_more: false,
|
has_more: false,
|
||||||
|
|||||||
@@ -489,7 +489,7 @@ export interface CursorPaginationRequest {
|
|||||||
*/
|
*/
|
||||||
export interface CursorPaginationResponse<T> {
|
export interface CursorPaginationResponse<T> {
|
||||||
/** 数据项列表 */
|
/** 数据项列表 */
|
||||||
items: T[];
|
list: T[];
|
||||||
/** 下一页游标 */
|
/** 下一页游标 */
|
||||||
next_cursor: string | null;
|
next_cursor: string | null;
|
||||||
/** 上一页游标 */
|
/** 上一页游标 */
|
||||||
|
|||||||
Reference in New Issue
Block a user