- Add BaseManager/CachedManager base classes to eliminate duplicate cache logic - Add postListSources.ts with IPostListPagedSource interface - Add groupListSources.ts with IGroupListPagedSource interface - Refactor postManager to use CachedManager and Sources pattern - Refactor groupManager to use CachedManager and Sources pattern - Clean up duplicate methods in groupService.ts - Fix TypeScript errors and remove mock data
138 lines
3.8 KiB
TypeScript
138 lines
3.8 KiB
TypeScript
/**
|
||
* 帖子列表数据源抽象:游标、偏移分页实现同一契约,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<PostListPage>;
|
||
/** 至少成功 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<PostListPage> {
|
||
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<PostListPage> {
|
||
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);
|
||
}
|