/** * 帖子列表数据源抽象:游标、偏移分页实现同一契约,PostManager 只依赖接口。 */ import { Post } from '../types'; import { postService } from '../services/postService'; import type { CursorPaginationRequest } from '../types/dto'; export const POST_LIST_PAGE_SIZE = 20; /** 帖子列表类型 */ export type PostListTab = 'hot' | 'latest' | 'follow'; /** 单次拉取结果(一页或一批) */ export interface PostListPage { items: Post[]; /** 在当前源上是否还能再 loadNext 一页 */ hasMore: boolean; } /** * 分页式帖子列表源:restart 后开始新序列;首次 loadNext 为第一页,之后为后续页。 */ export interface IPostListPagedSource { restart(): void; loadNext(): Promise; /** 至少成功 loadNext 过一次且仍有下一页时为 true */ readonly hasMore: boolean; } /** * 远端帖子列表(游标分页) */ export class NetworkCursorPostListPagedSource implements IPostListPagedSource { private nextCursor: string | null = null; private hasMoreAfterLastLoad = false; private loadedOnce = false; private readonly pageSize: number; private readonly tab?: PostListTab; private readonly channelId?: string; constructor( options: { tab?: PostListTab; channelId?: string; pageSize?: number } = {} ) { this.tab = options.tab; this.channelId = options.channelId; this.pageSize = options.pageSize ?? POST_LIST_PAGE_SIZE; } restart(): void { this.nextCursor = null; this.hasMoreAfterLastLoad = false; this.loadedOnce = false; } get hasMore(): boolean { return this.loadedOnce && this.hasMoreAfterLastLoad; } async loadNext(): Promise { const params: CursorPaginationRequest = { cursor: this.nextCursor ?? '', page_size: this.pageSize, post_type: this.tab, }; const result = await postService.getPostsCursor(params); this.hasMoreAfterLastLoad = result.has_more ?? false; this.nextCursor = result.next_cursor ?? null; this.loadedOnce = true; return { items: result.list ?? [], hasMore: this.hasMoreAfterLastLoad }; } } /** * 远端帖子列表(偏移分页) */ export class NetworkOffsetPostListPagedSource implements IPostListPagedSource { private nextPage = 1; private hasMoreAfterLastLoad = false; private loadedOnce = false; private readonly pageSize: number; private readonly tab?: PostListTab; private readonly channelId?: string; constructor( options: { tab?: PostListTab; channelId?: string; pageSize?: number } = {} ) { this.tab = options.tab; this.channelId = options.channelId; this.pageSize = options.pageSize ?? POST_LIST_PAGE_SIZE; } restart(): void { this.nextPage = 1; this.hasMoreAfterLastLoad = false; this.loadedOnce = false; } get hasMore(): boolean { return this.loadedOnce && this.hasMoreAfterLastLoad; } async loadNext(): Promise { const result = await postService.getPosts( this.nextPage, this.pageSize, this.tab, this.channelId ); const items = result.list ?? []; const total = result.total ?? 0; const totalPages = result.total_pages ?? 1; this.hasMoreAfterLastLoad = this.nextPage < totalPages; this.nextPage++; this.loadedOnce = true; return { items, hasMore: this.hasMoreAfterLastLoad }; } } /** 远端帖子列表分页策略 */ export type RemotePostListSourceKind = 'cursor' | 'offset'; export function createRemotePostListSource( kind: RemotePostListSourceKind = 'cursor', options: { tab?: PostListTab; channelId?: string; pageSize?: number } = {} ): IPostListPagedSource { if (kind === 'offset') { return new NetworkOffsetPostListPagedSource(options); } return new NetworkCursorPostListPagedSource(options); }