refactor: restructure core architecture and responsive system
This commit implements a major architectural refactor to improve modularity, type safety, and maintainability across the codebase.
Key changes include:
- **Responsive System Refactor**: Migrated from a monolithic `useResponsive` hook to a set of specialized, granular hooks (`useBreakpoint`, `useOrientation`, `usePlatform`, etc.) located in `src/presentation/hooks/responsive`. This reduces unnecessary re-renders and improves developer experience.
- **Core Service Restructuring**: Introduced a new directory structure for services, including dedicated folders for `datasources`, `mappers`, and domain-specific types (`message`, `post`).
- **Data Layer Improvements**:
- Centralized JSON parsing logic in `src/database/core/jsonUtils.ts`.
- Cleaned up repository implementations by removing redundant local utility functions.
- Refactored `MessageMapper` to use the new centralized JSON utility.
- **Type System Cleanup**:
- Decomposed the large `src/types/dto.ts` into a modular `src/types/dto/` directory.
- Simplified `src/types/index.ts` and introduced backward-compatible aliases for core entities.
- **Utility Consolidation**:
- Created a centralized `src/utils/formatTime.ts` to replace fragmented date formatting logic across various screens and components.
- Removed deprecated responsive utility files in favor of the new hook-based system.
- **Service Logic Refinement**: Refactored `ApiClient` and `WebSocketService` to use a centralized `showVerificationModal` service, removing duplicated state management for verification prompts.
This commit is contained in:
202
src/utils/cache/types.ts
vendored
Normal file
202
src/utils/cache/types.ts
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* 媒体缓存类型定义
|
||||
* @module src/utils/cache/types
|
||||
* @description 定义媒体缓存相关的类型、枚举和接口
|
||||
*/
|
||||
|
||||
/**
|
||||
* 媒体类型枚举
|
||||
* @enum {string}
|
||||
* @description 支持的媒体类型
|
||||
*/
|
||||
export enum MediaType {
|
||||
IMAGE = 'IMAGE',
|
||||
VIDEO = 'VIDEO',
|
||||
AUDIO = 'AUDIO',
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理触发器枚举
|
||||
* @enum {string}
|
||||
* @description 触发清理操作的来源
|
||||
*/
|
||||
export enum CleanupTrigger {
|
||||
STARTUP = 'startup',
|
||||
SCHEDULED = 'scheduled',
|
||||
MANUAL = 'manual',
|
||||
LOW_STORAGE = 'low_storage',
|
||||
CONVERSATION_DELETED = 'conversation_deleted',
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存条目接口
|
||||
* @interface CacheEntry
|
||||
* @description 单个媒体缓存条目的信息
|
||||
*/
|
||||
export interface CacheEntry {
|
||||
/** 缓存键值 */
|
||||
key: string;
|
||||
/** 媒体类型 */
|
||||
type: MediaType;
|
||||
/** 原始URI */
|
||||
uri: string;
|
||||
/** 本地存储路径 */
|
||||
localPath: string;
|
||||
/** 文件大小(字节) */
|
||||
size: number;
|
||||
/** 所属会话ID(可选) */
|
||||
conversationId?: string;
|
||||
/** 关联消息ID(可选) */
|
||||
messageId?: string;
|
||||
/** 创建时间 */
|
||||
createdAt: number;
|
||||
/** 最后访问时间 */
|
||||
lastAccessedAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存统计接口
|
||||
* @interface CacheStats
|
||||
* @description 媒体缓存的整体统计信息
|
||||
*/
|
||||
export interface CacheStats {
|
||||
/** 总缓存大小(字节) */
|
||||
totalSize: number;
|
||||
/** 图片缓存数量 */
|
||||
imageCount: number;
|
||||
/** 视频缓存数量 */
|
||||
videoCount: number;
|
||||
/** 音频缓存数量 */
|
||||
audioCount: number;
|
||||
/** 总条目数 */
|
||||
totalEntries: number;
|
||||
/** 最早缓存项时间 */
|
||||
oldestItem: number;
|
||||
/** 最新缓存项时间 */
|
||||
newestItem: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理策略接口
|
||||
* @interface CleanupPolicy
|
||||
* @description 定义缓存清理的规则和限制
|
||||
*/
|
||||
export interface CleanupPolicy {
|
||||
/** 最大缓存时间(毫秒) */
|
||||
maxAge: number;
|
||||
/** 最大缓存大小(字节) */
|
||||
maxSize: number;
|
||||
/** 最大条目数 */
|
||||
maxEntries: number;
|
||||
/** 启动时是否清理 */
|
||||
cleanupOnStart: boolean;
|
||||
/** 定期清理间隔(毫秒) */
|
||||
cleanupInterval: number;
|
||||
/** 是否启用LRU清理 */
|
||||
enableLRU: boolean;
|
||||
/** 低存储空间阈值(字节) */
|
||||
lowStorageThreshold: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理结果接口
|
||||
* @interface CleanupResult
|
||||
* @description 清理操作的执行结果
|
||||
*/
|
||||
export interface CleanupResult {
|
||||
/** 触发器类型 */
|
||||
trigger: CleanupTrigger;
|
||||
/** 执行时间戳 */
|
||||
timestamp: number;
|
||||
/** 删除条目数量 */
|
||||
deletedCount: number;
|
||||
/** 释放空间大小(字节) */
|
||||
freedSize: number;
|
||||
/** 错误信息列表 */
|
||||
errors: string[];
|
||||
/** 清理的条目详情 */
|
||||
deletedItems?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 媒体缓存配置接口
|
||||
* @interface MediaCacheConfig
|
||||
* @description 媒体缓存的全局配置
|
||||
*/
|
||||
export interface MediaCacheConfig {
|
||||
/** 图片配置 */
|
||||
image: {
|
||||
/** 内存缓存数量 */
|
||||
maxMemoryCacheSize: number;
|
||||
/** 磁盘缓存大小(MB) */
|
||||
maxDiskCacheSize: number;
|
||||
/** 缓存有效期(小时) */
|
||||
maxAge: number;
|
||||
/** 会话内媒体保留时间(天) */
|
||||
maxAgeForConversation: number;
|
||||
};
|
||||
/** 视频配置 */
|
||||
video: {
|
||||
/** 磁盘缓存大小(MB) */
|
||||
maxDiskCacheSize: number;
|
||||
/** 缓存有效期(小时) */
|
||||
maxAge: number;
|
||||
/** 是否自动预加载 */
|
||||
autoPreload: boolean;
|
||||
};
|
||||
/** 音频配置 */
|
||||
audio: {
|
||||
/** 磁盘缓存大小(MB) */
|
||||
maxDiskCacheSize: number;
|
||||
/** 缓存有效期(天) */
|
||||
maxAge: number;
|
||||
};
|
||||
/** 全局配置 */
|
||||
global: {
|
||||
/** 启动时检查清理 */
|
||||
checkOnStartup: boolean;
|
||||
/** 定期检查间隔(小时) */
|
||||
checkInterval: number;
|
||||
/** 低存储空间阈值(MB) */
|
||||
lowStorageThreshold: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认媒体缓存配置
|
||||
*/
|
||||
export const DEFAULT_MEDIA_CACHE_CONFIG: MediaCacheConfig = {
|
||||
image: {
|
||||
maxMemoryCacheSize: 50,
|
||||
maxDiskCacheSize: 500,
|
||||
maxAge: 168, // 7 days
|
||||
maxAgeForConversation: 30,
|
||||
},
|
||||
video: {
|
||||
maxDiskCacheSize: 1000,
|
||||
maxAge: 72, // 3 days
|
||||
autoPreload: false,
|
||||
},
|
||||
audio: {
|
||||
maxDiskCacheSize: 200,
|
||||
maxAge: 168, // 7 days
|
||||
},
|
||||
global: {
|
||||
checkOnStartup: true,
|
||||
checkInterval: 24,
|
||||
lowStorageThreshold: 500,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认清理策略
|
||||
*/
|
||||
export const DEFAULT_CLEANUP_POLICY: CleanupPolicy = {
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 7天
|
||||
maxSize: 500 * 1024 * 1024, // 500MB
|
||||
maxEntries: 1000,
|
||||
cleanupOnStart: true,
|
||||
cleanupInterval: 24 * 60 * 60 * 1000, // 24小时
|
||||
enableLRU: true,
|
||||
lowStorageThreshold: 500 * 1024 * 1024, // 500MB
|
||||
};
|
||||
268
src/utils/diff/messageDiffCalculator.ts
Normal file
268
src/utils/diff/messageDiffCalculator.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* 消息差异计算器
|
||||
* 计算两个消息列表之间的差异,支持增量更新
|
||||
*/
|
||||
|
||||
import {
|
||||
MessageIdentifier,
|
||||
DiffResult,
|
||||
MessageComparator,
|
||||
MessageSorter,
|
||||
DiffConfig,
|
||||
DEFAULT_DIFF_CONFIG,
|
||||
} from './messageTypes';
|
||||
|
||||
/**
|
||||
* 消息差异计算器类
|
||||
* 用于高效计算消息列表的变化
|
||||
*/
|
||||
export class MessageDiffCalculator<T extends MessageIdentifier> {
|
||||
private config: Required<DiffConfig>;
|
||||
private previousMessages: Map<string, T> = new Map();
|
||||
private previousOrder: string[] = [];
|
||||
|
||||
constructor(config: DiffConfig = {}) {
|
||||
this.config = { ...DEFAULT_DIFF_CONFIG, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个消息列表的差异
|
||||
* @param oldList 旧消息列表
|
||||
* @param newList 新消息列表
|
||||
* @returns 差异结果
|
||||
*/
|
||||
calculateDiff(oldList: T[], newList: T[]): DiffResult<T> {
|
||||
const result: DiffResult<T> = {
|
||||
added: [],
|
||||
updated: [],
|
||||
deleted: [],
|
||||
moved: [],
|
||||
unchanged: [],
|
||||
shouldReset: false,
|
||||
};
|
||||
|
||||
// 如果旧列表为空,直接返回新列表作为新增
|
||||
if (oldList.length === 0) {
|
||||
result.added = [...newList];
|
||||
this.updateCache(newList);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 如果新列表为空,表示全部删除
|
||||
if (newList.length === 0) {
|
||||
result.deleted = oldList.map((msg, index) => ({ message: msg, index }));
|
||||
this.clearCache();
|
||||
return result;
|
||||
}
|
||||
|
||||
// 检查是否需要重置(变化太大)
|
||||
const changeRatio = this.calculateChangeRatio(oldList, newList);
|
||||
if (changeRatio > 0.5) {
|
||||
result.shouldReset = true;
|
||||
result.added = [...newList];
|
||||
this.updateCache(newList);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 构建旧列表的索引映射
|
||||
const oldIndexMap = new Map<string, number>();
|
||||
oldList.forEach((msg, index) => {
|
||||
oldIndexMap.set(msg.id, index);
|
||||
});
|
||||
|
||||
// 构建新列表的索引映射
|
||||
const newIndexMap = new Map<string, number>();
|
||||
newList.forEach((msg, index) => {
|
||||
newIndexMap.set(msg.id, index);
|
||||
});
|
||||
|
||||
// 检测新增、更新和未变更的消息
|
||||
const processedIds = new Set<string>();
|
||||
|
||||
for (let newIndex = 0; newIndex < newList.length; newIndex++) {
|
||||
const newMsg = newList[newIndex];
|
||||
const oldIndex = oldIndexMap.get(newMsg.id);
|
||||
|
||||
if (oldIndex === undefined) {
|
||||
// 新增消息
|
||||
result.added.push(newMsg);
|
||||
} else {
|
||||
const oldMsg = oldList[oldIndex];
|
||||
processedIds.add(newMsg.id);
|
||||
|
||||
// 检测是否有更新
|
||||
const changes = this.detectChanges(oldMsg, newMsg);
|
||||
if (changes && Object.keys(changes).length > 0) {
|
||||
result.updated.push({
|
||||
message: newMsg,
|
||||
index: oldIndex,
|
||||
changes,
|
||||
});
|
||||
} else {
|
||||
result.unchanged.push(newMsg);
|
||||
}
|
||||
|
||||
// 检测是否移动
|
||||
if (oldIndex !== newIndex) {
|
||||
result.moved.push({
|
||||
message: newMsg,
|
||||
fromIndex: oldIndex,
|
||||
toIndex: newIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检测删除的消息
|
||||
for (let oldIndex = 0; oldIndex < oldList.length; oldIndex++) {
|
||||
const oldMsg = oldList[oldIndex];
|
||||
if (!processedIds.has(oldMsg.id)) {
|
||||
result.deleted.push({ message: oldMsg, index: oldIndex });
|
||||
}
|
||||
}
|
||||
|
||||
this.updateCache(newList);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算增量差异(基于缓存的上次状态)
|
||||
* @param newList 新消息列表
|
||||
* @returns 差异结果
|
||||
*/
|
||||
calculateIncrementalDiff(newList: T[]): DiffResult<T> {
|
||||
const oldList = Array.from(this.previousMessages.values());
|
||||
return this.calculateDiff(oldList, newList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算变化比例
|
||||
* @param oldList 旧列表
|
||||
* @param newList 新列表
|
||||
* @returns 变化比例(0-1)
|
||||
*/
|
||||
private calculateChangeRatio(oldList: T[], newList: T[]): number {
|
||||
if (oldList.length === 0 && newList.length === 0) return 0;
|
||||
if (oldList.length === 0 || newList.length === 0) return 1;
|
||||
|
||||
const oldIds = new Set(oldList.map(m => m.id));
|
||||
const newIds = new Set(newList.map(m => m.id));
|
||||
|
||||
let commonCount = 0;
|
||||
oldIds.forEach(id => {
|
||||
if (newIds.has(id)) {
|
||||
commonCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const maxLength = Math.max(oldList.length, newList.length);
|
||||
return 1 - commonCount / maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测消息变化
|
||||
* @param oldMsg 旧消息
|
||||
* @param newMsg 新消息
|
||||
* @returns 变化字段
|
||||
*/
|
||||
private detectChanges(oldMsg: T, newMsg: T): Partial<T> | null {
|
||||
const changes: Partial<T> = {};
|
||||
let hasChanges = false;
|
||||
|
||||
for (const key of Object.keys(newMsg) as Array<keyof T>) {
|
||||
if (key === 'id') continue; // ID 不变
|
||||
|
||||
const oldValue = oldMsg[key];
|
||||
const newValue = newMsg[key];
|
||||
|
||||
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
||||
(changes as any)[key] = newValue;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges ? changes : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新缓存
|
||||
* @param messages 消息列表
|
||||
*/
|
||||
private updateCache(messages: T[]): void {
|
||||
this.previousMessages.clear();
|
||||
this.previousOrder = [];
|
||||
|
||||
for (const msg of messages) {
|
||||
this.previousMessages.set(msg.id, msg);
|
||||
this.previousOrder.push(msg.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
private clearCache(): void {
|
||||
this.previousMessages.clear();
|
||||
this.previousOrder = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置配置
|
||||
* @param config 配置
|
||||
*/
|
||||
setConfig(config: Partial<DiffConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
* @returns 当前配置
|
||||
*/
|
||||
getConfig(): Required<DiffConfig> {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置计算器状态
|
||||
*/
|
||||
reset(): void {
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的消息数量
|
||||
* @returns 缓存数量
|
||||
*/
|
||||
getCachedCount(): number {
|
||||
return this.previousMessages.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息是否在缓存中
|
||||
* @param messageId 消息 ID
|
||||
* @returns 是否存在
|
||||
*/
|
||||
hasMessage(messageId: string): boolean {
|
||||
return this.previousMessages.has(messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存中的消息
|
||||
* @param messageId 消息 ID
|
||||
* @returns 消息或 undefined
|
||||
*/
|
||||
getCachedMessage(messageId: string): T | undefined {
|
||||
return this.previousMessages.get(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建差异计算器实例的工厂函数
|
||||
* @param config 配置
|
||||
* @returns MessageDiffCalculator 实例
|
||||
*/
|
||||
export function createMessageDiffCalculator<T extends MessageIdentifier>(
|
||||
config?: DiffConfig
|
||||
): MessageDiffCalculator<T> {
|
||||
return new MessageDiffCalculator<T>(config);
|
||||
}
|
||||
209
src/utils/diff/messageTypes.ts
Normal file
209
src/utils/diff/messageTypes.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 差异更新类型定义
|
||||
* 用于消息列表的增量更新,减少 UI 重渲染次数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 消息更新类型枚举
|
||||
*/
|
||||
export enum MessageUpdateType {
|
||||
/** 添加单条消息 */
|
||||
ADD = 'ADD',
|
||||
/** 更新单条消息 */
|
||||
UPDATE = 'UPDATE',
|
||||
/** 删除单条消息 */
|
||||
DELETE = 'DELETE',
|
||||
/** 移动消息位置 */
|
||||
MOVE = 'MOVE',
|
||||
/** 批量添加 */
|
||||
BATCH_ADD = 'BATCH_ADD',
|
||||
/** 批量更新 */
|
||||
BATCH_UPDATE = 'BATCH_UPDATE',
|
||||
/** 批量删除 */
|
||||
BATCH_DELETE = 'BATCH_DELETE',
|
||||
/** 重置整个列表 */
|
||||
RESET = 'RESET',
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息唯一标识接口
|
||||
*/
|
||||
export interface MessageIdentifier {
|
||||
/** 消息唯一 ID */
|
||||
id: string;
|
||||
/** 消息序列号(可选,用于排序) */
|
||||
seq?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础更新操作接口
|
||||
*/
|
||||
export interface BaseMessageUpdate {
|
||||
/** 更新类型 */
|
||||
type: MessageUpdateType;
|
||||
/** 更新时间戳 */
|
||||
timestamp: number;
|
||||
/** 会话 ID(可选) */
|
||||
conversationId?: string;
|
||||
/** 载荷数据(可选,用于合并更新) */
|
||||
payload?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加单条消息更新
|
||||
*/
|
||||
export interface AddMessageUpdate extends BaseMessageUpdate {
|
||||
type: MessageUpdateType.ADD;
|
||||
/** 消息数据 */
|
||||
message: MessageIdentifier;
|
||||
/** 插入位置索引(可选,默认追加到末尾) */
|
||||
index?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单条消息更新
|
||||
*/
|
||||
export interface UpdateMessageUpdate extends BaseMessageUpdate {
|
||||
type: MessageUpdateType.UPDATE;
|
||||
/** 消息 ID */
|
||||
messageId: string;
|
||||
/** 更新的字段 */
|
||||
updates: Partial<MessageIdentifier>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单条消息更新
|
||||
*/
|
||||
export interface DeleteMessageUpdate extends BaseMessageUpdate {
|
||||
type: MessageUpdateType.DELETE;
|
||||
/** 消息 ID */
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动消息更新
|
||||
*/
|
||||
export interface MoveMessageUpdate extends BaseMessageUpdate {
|
||||
type: MessageUpdateType.MOVE;
|
||||
/** 消息 ID */
|
||||
messageId: string;
|
||||
/** 目标位置索引 */
|
||||
toIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加消息更新
|
||||
*/
|
||||
export interface BatchAddMessageUpdate extends BaseMessageUpdate {
|
||||
type: MessageUpdateType.BATCH_ADD;
|
||||
/** 消息列表 */
|
||||
messages: MessageIdentifier[];
|
||||
/** 插入位置索引(可选,默认追加到末尾) */
|
||||
startIndex?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新消息更新
|
||||
*/
|
||||
export interface BatchUpdateMessageUpdate extends BaseMessageUpdate {
|
||||
type: MessageUpdateType.BATCH_UPDATE;
|
||||
/** 批量更新项 */
|
||||
updates: Array<{
|
||||
messageId: string;
|
||||
changes: Partial<MessageIdentifier>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除消息更新
|
||||
*/
|
||||
export interface BatchDeleteMessageUpdate extends BaseMessageUpdate {
|
||||
type: MessageUpdateType.BATCH_DELETE;
|
||||
/** 要删除的消息 ID 列表 */
|
||||
messageIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置消息列表更新
|
||||
*/
|
||||
export interface ResetMessageUpdate extends BaseMessageUpdate {
|
||||
type: MessageUpdateType.RESET;
|
||||
/** 新的消息列表 */
|
||||
messages: MessageIdentifier[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息更新联合类型
|
||||
*/
|
||||
export type MessageUpdate =
|
||||
| AddMessageUpdate
|
||||
| UpdateMessageUpdate
|
||||
| DeleteMessageUpdate
|
||||
| MoveMessageUpdate
|
||||
| BatchAddMessageUpdate
|
||||
| BatchUpdateMessageUpdate
|
||||
| BatchDeleteMessageUpdate
|
||||
| ResetMessageUpdate;
|
||||
|
||||
/**
|
||||
* 差异配置接口
|
||||
*/
|
||||
export interface DiffConfig {
|
||||
/** 批量处理间隔(毫秒),默认 16ms(60fps) */
|
||||
batchInterval?: number;
|
||||
/** 最大批量大小,默认 100 */
|
||||
maxBatchSize?: number;
|
||||
/** 是否启用去重,默认 true */
|
||||
enableDeduplication?: boolean;
|
||||
/** 是否合并相同消息的多次更新,默认 true */
|
||||
enableMerge?: boolean;
|
||||
/** 消息唯一标识字段,默认 'id' */
|
||||
idField?: string;
|
||||
/** 排序字段,默认 'seq' */
|
||||
sortField?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 差异计算结果接口
|
||||
*/
|
||||
export interface DiffResult<T extends MessageIdentifier> {
|
||||
/** 新增的消息 */
|
||||
added: T[];
|
||||
/** 更新的消息 */
|
||||
updated: Array<{ message: T; index: number; changes: Partial<T> }>;
|
||||
/** 删除的消息 */
|
||||
deleted: Array<{ message: T; index: number }>;
|
||||
/** 移动的消息 */
|
||||
moved: Array<{ message: T; fromIndex: number; toIndex: number }>;
|
||||
/** 未变更的消息 */
|
||||
unchanged: T[];
|
||||
/** 是否需要重置 */
|
||||
shouldReset: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新监听器类型
|
||||
*/
|
||||
export type BatchUpdateListener = (updates: MessageUpdate[]) => void;
|
||||
|
||||
/**
|
||||
* 消息比较函数类型
|
||||
*/
|
||||
export type MessageComparator<T extends MessageIdentifier> = (a: T, b: T) => boolean;
|
||||
|
||||
/**
|
||||
* 消息排序函数类型
|
||||
*/
|
||||
export type MessageSorter<T extends MessageIdentifier> = (a: T, b: T) => number;
|
||||
|
||||
/**
|
||||
* 默认配置常量
|
||||
*/
|
||||
export const DEFAULT_DIFF_CONFIG: Required<DiffConfig> = {
|
||||
batchInterval: 16,
|
||||
maxBatchSize: 100,
|
||||
enableDeduplication: true,
|
||||
enableMerge: true,
|
||||
idField: 'id',
|
||||
sortField: 'seq',
|
||||
};
|
||||
430
src/utils/diff/postDiffCalculator.ts
Normal file
430
src/utils/diff/postDiffCalculator.ts
Normal file
@@ -0,0 +1,430 @@
|
||||
/**
|
||||
* 帖子差异计算器
|
||||
* 计算两个帖子列表之间的差异,支持增量更新
|
||||
*/
|
||||
|
||||
import {
|
||||
PostIdentifier,
|
||||
PostDiffResult,
|
||||
PostDiffConfig,
|
||||
DEFAULT_POST_DIFF_CONFIG,
|
||||
PostComparator,
|
||||
PostSorter,
|
||||
PostDiffStats,
|
||||
} from './postTypes';
|
||||
|
||||
/**
|
||||
* 帖子差异计算器类
|
||||
* 用于高效计算帖子列表的变化
|
||||
*/
|
||||
export class PostDiffCalculator<T extends PostIdentifier> {
|
||||
private config: Required<PostDiffConfig>;
|
||||
private previousPosts: Map<string, T> = new Map();
|
||||
private previousOrder: string[] = [];
|
||||
private stats: PostDiffStats = {
|
||||
totalBatches: 0,
|
||||
totalUpdates: 0,
|
||||
averageBatchSize: 0,
|
||||
lastBatchTime: 0,
|
||||
addedCount: 0,
|
||||
updatedCount: 0,
|
||||
deletedCount: 0,
|
||||
};
|
||||
|
||||
constructor(config: PostDiffConfig = {}) {
|
||||
this.config = { ...DEFAULT_POST_DIFF_CONFIG, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个帖子列表的差异
|
||||
* @param oldList 旧帖子列表
|
||||
* @param newList 新帖子列表
|
||||
* @returns 差异结果
|
||||
*/
|
||||
calculateDiff(oldList: T[], newList: T[]): PostDiffResult<T> {
|
||||
const result: PostDiffResult<T> = {
|
||||
added: [],
|
||||
updated: [],
|
||||
deleted: [],
|
||||
moved: [],
|
||||
unchanged: [],
|
||||
shouldReset: false,
|
||||
};
|
||||
|
||||
// 如果旧列表为空,直接返回新列表作为新增
|
||||
if (oldList.length === 0) {
|
||||
result.added = [...newList];
|
||||
this.updateCache(newList);
|
||||
this.updateStats(0, newList.length, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 如果新列表为空,表示全部删除
|
||||
if (newList.length === 0) {
|
||||
result.deleted = oldList.map((post, index) => ({ post, index }));
|
||||
this.clearCache();
|
||||
this.updateStats(0, 0, oldList.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 检查是否需要重置(变化太大)
|
||||
const changeRatio = this.calculateChangeRatio(oldList, newList);
|
||||
if (changeRatio > this.config.resetThreshold) {
|
||||
result.shouldReset = true;
|
||||
result.added = [...newList];
|
||||
this.updateCache(newList);
|
||||
this.updateStats(0, newList.length, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 构建旧列表的索引映射
|
||||
const oldIndexMap = new Map<string, number>();
|
||||
oldList.forEach((post, index) => {
|
||||
oldIndexMap.set(post.id, index);
|
||||
});
|
||||
|
||||
// 构建新列表的索引映射
|
||||
const newIndexMap = new Map<string, number>();
|
||||
newList.forEach((post, index) => {
|
||||
newIndexMap.set(post.id, index);
|
||||
});
|
||||
|
||||
// 检测新增、更新和未变更的帖子
|
||||
const processedIds = new Set<string>();
|
||||
let addedCount = 0;
|
||||
let updatedCount = 0;
|
||||
|
||||
for (let newIndex = 0; newIndex < newList.length; newIndex++) {
|
||||
const newPost = newList[newIndex];
|
||||
const oldIndex = oldIndexMap.get(newPost.id);
|
||||
|
||||
if (oldIndex === undefined) {
|
||||
// 新增帖子
|
||||
result.added.push(newPost);
|
||||
addedCount++;
|
||||
} else {
|
||||
const oldPost = oldList[oldIndex];
|
||||
processedIds.add(newPost.id);
|
||||
|
||||
// 检测是否有更新
|
||||
const changes = this.detectChanges(oldPost, newPost);
|
||||
if (changes && Object.keys(changes).length > 0) {
|
||||
result.updated.push({
|
||||
post: newPost,
|
||||
index: oldIndex,
|
||||
changes,
|
||||
});
|
||||
updatedCount++;
|
||||
} else {
|
||||
result.unchanged.push(newPost);
|
||||
}
|
||||
|
||||
// 检测是否移动
|
||||
if (oldIndex !== newIndex) {
|
||||
result.moved.push({
|
||||
post: newPost,
|
||||
fromIndex: oldIndex,
|
||||
toIndex: newIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检测删除的帖子
|
||||
let deletedCount = 0;
|
||||
for (let oldIndex = 0; oldIndex < oldList.length; oldIndex++) {
|
||||
const oldPost = oldList[oldIndex];
|
||||
if (!processedIds.has(oldPost.id)) {
|
||||
result.deleted.push({ post: oldPost, index: oldIndex });
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateCache(newList);
|
||||
this.updateStats(addedCount, updatedCount, deletedCount);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算增量差异(基于缓存的上次状态)
|
||||
* @param newList 新帖子列表
|
||||
* @returns 差异结果
|
||||
*/
|
||||
calculateIncrementalDiff(newList: T[]): PostDiffResult<T> {
|
||||
const oldList = Array.from(this.previousPosts.values());
|
||||
// 按照之前的顺序排序
|
||||
oldList.sort((a, b) => {
|
||||
const indexA = this.previousOrder.indexOf(a.id);
|
||||
const indexB = this.previousOrder.indexOf(b.id);
|
||||
return indexA - indexB;
|
||||
});
|
||||
return this.calculateDiff(oldList, newList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算单个帖子的变化
|
||||
* @param oldPost 旧帖子
|
||||
* @param newPost 新帖子
|
||||
* @returns 变化的字段
|
||||
*/
|
||||
detectChanges(oldPost: T, newPost: T): Partial<T> | null {
|
||||
const changes: Partial<T> = {};
|
||||
let hasChanges = false;
|
||||
|
||||
// 如果配置了追踪特定字段,只检测这些字段
|
||||
const fieldsToCheck = this.config.detectPartialUpdates
|
||||
? this.config.trackedFields
|
||||
: (Object.keys(newPost) as Array<keyof T>);
|
||||
|
||||
for (const key of fieldsToCheck) {
|
||||
if (key === 'id') continue; // ID 不变
|
||||
|
||||
const oldValue = (oldPost as any)[key];
|
||||
const newValue = (newPost as any)[key];
|
||||
|
||||
// 深度比较
|
||||
if (this.hasValueChanged(oldValue, newValue)) {
|
||||
(changes as any)[key] = newValue;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges ? changes : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测值是否变化
|
||||
* @param oldValue 旧值
|
||||
* @param newValue 新值
|
||||
* @returns 是否变化
|
||||
*/
|
||||
private hasValueChanged(oldValue: any, newValue: any): boolean {
|
||||
// 简单类型直接比较
|
||||
if (typeof oldValue !== 'object' || oldValue === null || newValue === null) {
|
||||
return oldValue !== newValue;
|
||||
}
|
||||
|
||||
// 数组比较
|
||||
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
|
||||
if (oldValue.length !== newValue.length) return true;
|
||||
return JSON.stringify(oldValue) !== JSON.stringify(newValue);
|
||||
}
|
||||
|
||||
// 对象比较
|
||||
return JSON.stringify(oldValue) !== JSON.stringify(newValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算变化比例
|
||||
* @param oldList 旧列表
|
||||
* @param newList 新列表
|
||||
* @returns 变化比例(0-1)
|
||||
*/
|
||||
private calculateChangeRatio(oldList: T[], newList: T[]): number {
|
||||
if (oldList.length === 0 && newList.length === 0) return 0;
|
||||
if (oldList.length === 0 || newList.length === 0) return 1;
|
||||
|
||||
const oldIds = new Set(oldList.map(p => p.id));
|
||||
const newIds = new Set(newList.map(p => p.id));
|
||||
|
||||
let commonCount = 0;
|
||||
oldIds.forEach(id => {
|
||||
if (newIds.has(id)) {
|
||||
commonCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const maxLength = Math.max(oldList.length, newList.length);
|
||||
return 1 - commonCount / maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新缓存
|
||||
* @param posts 帖子列表
|
||||
*/
|
||||
private updateCache(posts: T[]): void {
|
||||
this.previousPosts.clear();
|
||||
this.previousOrder = [];
|
||||
|
||||
for (const post of posts) {
|
||||
this.previousPosts.set(post.id, post);
|
||||
this.previousOrder.push(post.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
private clearCache(): void {
|
||||
this.previousPosts.clear();
|
||||
this.previousOrder = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新统计信息
|
||||
*/
|
||||
private updateStats(addedCount: number, updatedCount: number, deletedCount: number): void {
|
||||
this.stats.totalBatches++;
|
||||
const totalChanges = addedCount + updatedCount + deletedCount;
|
||||
this.stats.totalUpdates += totalChanges;
|
||||
this.stats.averageBatchSize = this.stats.totalUpdates / this.stats.totalBatches;
|
||||
this.stats.lastBatchTime = Date.now();
|
||||
this.stats.addedCount += addedCount;
|
||||
this.stats.updatedCount += updatedCount;
|
||||
this.stats.deletedCount += deletedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置配置
|
||||
* @param config 配置
|
||||
*/
|
||||
setConfig(config: Partial<PostDiffConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
* @returns 当前配置
|
||||
*/
|
||||
getConfig(): Required<PostDiffConfig> {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置计算器状态
|
||||
*/
|
||||
reset(): void {
|
||||
this.clearCache();
|
||||
this.stats = {
|
||||
totalBatches: 0,
|
||||
totalUpdates: 0,
|
||||
averageBatchSize: 0,
|
||||
lastBatchTime: 0,
|
||||
addedCount: 0,
|
||||
updatedCount: 0,
|
||||
deletedCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的帖子数量
|
||||
* @returns 缓存数量
|
||||
*/
|
||||
getCachedCount(): number {
|
||||
return this.previousPosts.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查帖子是否在缓存中
|
||||
* @param postId 帖子 ID
|
||||
* @returns 是否存在
|
||||
*/
|
||||
hasPost(postId: string): boolean {
|
||||
return this.previousPosts.has(postId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存中的帖子
|
||||
* @param postId 帖子 ID
|
||||
* @returns 帖子或 undefined
|
||||
*/
|
||||
getCachedPost(postId: string): T | undefined {
|
||||
return this.previousPosts.get(postId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
* @returns 统计信息
|
||||
*/
|
||||
getStats(): PostDiffStats {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量检测帖子变化
|
||||
* @param posts 要检测的帖子列表
|
||||
* @returns 变化的帖子列表
|
||||
*/
|
||||
batchDetectChanges(posts: T[]): Array<{ post: T; changes: Partial<T> }> {
|
||||
const results: Array<{ post: T; changes: Partial<T> }> = [];
|
||||
|
||||
for (const post of posts) {
|
||||
const cachedPost = this.previousPosts.get(post.id);
|
||||
if (cachedPost) {
|
||||
const changes = this.detectChanges(cachedPost, post);
|
||||
if (changes && Object.keys(changes).length > 0) {
|
||||
results.push({ post, changes });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个帖子列表是否相同
|
||||
* @param list1 列表1
|
||||
* @param list2 列表2
|
||||
* @returns 是否相同
|
||||
*/
|
||||
areEqual(list1: T[], list2: T[]): boolean {
|
||||
if (list1.length !== list2.length) return false;
|
||||
|
||||
for (let i = 0; i < list1.length; i++) {
|
||||
if (list1[i].id !== list2[i].id) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取两个帖子列表的交集ID
|
||||
* @param list1 列表1
|
||||
* @param list2 列表2
|
||||
* @returns 交集ID集合
|
||||
*/
|
||||
getIntersection(list1: T[], list2: T[]): Set<string> {
|
||||
const ids1 = new Set(list1.map(p => p.id));
|
||||
const ids2 = new Set(list2.map(p => p.id));
|
||||
const intersection = new Set<string>();
|
||||
|
||||
ids1.forEach(id => {
|
||||
if (ids2.has(id)) {
|
||||
intersection.add(id);
|
||||
}
|
||||
});
|
||||
|
||||
return intersection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取两个帖子列表的差集ID
|
||||
* @param list1 列表1
|
||||
* @param list2 列表2
|
||||
* @returns 差集ID集合(在list1中但不在list2中)
|
||||
*/
|
||||
getDifference(list1: T[], list2: T[]): Set<string> {
|
||||
const ids1 = new Set(list1.map(p => p.id));
|
||||
const ids2 = new Set(list2.map(p => p.id));
|
||||
const difference = new Set<string>();
|
||||
|
||||
ids1.forEach(id => {
|
||||
if (!ids2.has(id)) {
|
||||
difference.add(id);
|
||||
}
|
||||
});
|
||||
|
||||
return difference;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建帖子差异计算器实例的工厂函数
|
||||
* @param config 配置
|
||||
* @returns PostDiffCalculator 实例
|
||||
*/
|
||||
export function createPostDiffCalculator<T extends PostIdentifier>(
|
||||
config?: PostDiffConfig
|
||||
): PostDiffCalculator<T> {
|
||||
return new PostDiffCalculator<T>(config);
|
||||
}
|
||||
325
src/utils/diff/postTypes.ts
Normal file
325
src/utils/diff/postTypes.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Post差异更新类型定义
|
||||
* 用于帖子列表的增量更新,减少 UI 重渲染次数
|
||||
*/
|
||||
|
||||
import { Post } from '../../core/entities/Post';
|
||||
|
||||
// ==================== 变化类型枚举 ====================
|
||||
|
||||
/**
|
||||
* 帖子变化类型枚举
|
||||
*/
|
||||
export enum PostChangeType {
|
||||
/** 新增帖子 */
|
||||
ADDED = 'added',
|
||||
/** 更新帖子 */
|
||||
UPDATED = 'updated',
|
||||
/** 删除帖子 */
|
||||
DELETED = 'deleted',
|
||||
}
|
||||
|
||||
/**
|
||||
* 帖子更新类型枚举
|
||||
*/
|
||||
export enum PostUpdateType {
|
||||
/** 添加单条帖子 */
|
||||
ADD = 'ADD',
|
||||
/** 更新单条帖子 */
|
||||
UPDATE = 'UPDATE',
|
||||
/** 删除单条帖子 */
|
||||
DELETE = 'DELETE',
|
||||
/** 移动帖子位置 */
|
||||
MOVE = 'MOVE',
|
||||
/** 批量添加 */
|
||||
BATCH_ADD = 'BATCH_ADD',
|
||||
/** 批量更新 */
|
||||
BATCH_UPDATE = 'BATCH_UPDATE',
|
||||
/** 批量删除 */
|
||||
BATCH_DELETE = 'BATCH_DELETE',
|
||||
/** 重置整个列表 */
|
||||
RESET = 'RESET',
|
||||
}
|
||||
|
||||
// ==================== 基础接口 ====================
|
||||
|
||||
/**
|
||||
* 帖子唯一标识接口
|
||||
*/
|
||||
export interface PostIdentifier {
|
||||
/** 帖子唯一 ID */
|
||||
id: string;
|
||||
/** 帖子创建时间(可选,用于排序) */
|
||||
createdAt?: string;
|
||||
/** 更新时间(可选,用于检测变化) */
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个帖子变化记录
|
||||
*/
|
||||
export interface PostChange<T extends PostIdentifier = Post> {
|
||||
/** 变化类型 */
|
||||
type: PostChangeType;
|
||||
/** 帖子数据 */
|
||||
post: T;
|
||||
/** 变化前的索引位置(用于删除和移动) */
|
||||
oldIndex?: number;
|
||||
/** 变化后的索引位置(用于新增和移动) */
|
||||
newIndex?: number;
|
||||
/** 变化的字段(用于更新) */
|
||||
changes?: Partial<T>;
|
||||
}
|
||||
|
||||
// ==================== 差异结果接口 ====================
|
||||
|
||||
/**
|
||||
* Post差异计算结果接口
|
||||
*/
|
||||
export interface PostDiffResult<T extends PostIdentifier = Post> {
|
||||
/** 新增的帖子 */
|
||||
added: T[];
|
||||
/** 更新的帖子(包含变化详情) */
|
||||
updated: Array<{
|
||||
post: T;
|
||||
index: number;
|
||||
changes: Partial<T>;
|
||||
}>;
|
||||
/** 删除的帖子 */
|
||||
deleted: Array<{
|
||||
post: T;
|
||||
index: number;
|
||||
}>;
|
||||
/** 移动的帖子 */
|
||||
moved: Array<{
|
||||
post: T;
|
||||
fromIndex: number;
|
||||
toIndex: number;
|
||||
}>;
|
||||
/** 未变更的帖子 */
|
||||
unchanged: T[];
|
||||
/** 是否需要重置(变化比例过大时) */
|
||||
shouldReset: boolean;
|
||||
}
|
||||
|
||||
// ==================== 更新操作接口 ====================
|
||||
|
||||
/**
|
||||
* 基础帖子更新操作接口
|
||||
*/
|
||||
export interface BasePostUpdate {
|
||||
/** 更新类型 */
|
||||
type: PostUpdateType;
|
||||
/** 更新时间戳 */
|
||||
timestamp: number;
|
||||
/** 社区 ID(可选) */
|
||||
channelId?: string;
|
||||
/** 载荷数据(可选,用于合并更新) */
|
||||
payload?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加单条帖子更新
|
||||
*/
|
||||
export interface AddPostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.ADD;
|
||||
/** 帖子数据 */
|
||||
post: PostIdentifier;
|
||||
/** 插入位置索引(可选,默认追加到末尾) */
|
||||
index?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单条帖子更新
|
||||
*/
|
||||
export interface UpdatePostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.UPDATE;
|
||||
/** 帖子 ID */
|
||||
postId: string;
|
||||
/** 更新的字段 */
|
||||
updates: Partial<PostIdentifier>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单条帖子更新
|
||||
*/
|
||||
export interface DeletePostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.DELETE;
|
||||
/** 帖子 ID */
|
||||
postId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动帖子更新
|
||||
*/
|
||||
export interface MovePostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.MOVE;
|
||||
/** 帖子 ID */
|
||||
postId: string;
|
||||
/** 目标位置索引 */
|
||||
toIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加帖子更新
|
||||
*/
|
||||
export interface BatchAddPostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.BATCH_ADD;
|
||||
/** 帖子列表 */
|
||||
posts: PostIdentifier[];
|
||||
/** 插入位置索引(可选,默认追加到末尾) */
|
||||
startIndex?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新帖子更新
|
||||
*/
|
||||
export interface BatchUpdatePostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.BATCH_UPDATE;
|
||||
/** 批量更新项 */
|
||||
updates: Array<{
|
||||
postId: string;
|
||||
changes: Partial<PostIdentifier>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除帖子更新
|
||||
*/
|
||||
export interface BatchDeletePostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.BATCH_DELETE;
|
||||
/** 要删除的帖子 ID 列表 */
|
||||
postIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置帖子列表更新
|
||||
*/
|
||||
export interface ResetPostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.RESET;
|
||||
/** 新的帖子列表 */
|
||||
posts: PostIdentifier[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 帖子更新联合类型
|
||||
*/
|
||||
export type PostUpdate =
|
||||
| AddPostUpdate
|
||||
| UpdatePostUpdate
|
||||
| DeletePostUpdate
|
||||
| MovePostUpdate
|
||||
| BatchAddPostUpdate
|
||||
| BatchUpdatePostUpdate
|
||||
| BatchDeletePostUpdate
|
||||
| ResetPostUpdate;
|
||||
|
||||
// ==================== 批量更新接口 ====================
|
||||
|
||||
/**
|
||||
* Post批量更新类型
|
||||
*/
|
||||
export interface PostUpdateBatch {
|
||||
/** 批次 ID */
|
||||
batchId: string;
|
||||
/** 更新列表 */
|
||||
updates: PostUpdate[];
|
||||
/** 创建时间戳 */
|
||||
createdAt: number;
|
||||
/** 是否已处理 */
|
||||
processed: boolean;
|
||||
}
|
||||
|
||||
// ==================== 配置接口 ====================
|
||||
|
||||
/**
|
||||
* Post差异配置接口
|
||||
*/
|
||||
export interface PostDiffConfig {
|
||||
/** 批量处理间隔(毫秒),默认 16ms(60fps) */
|
||||
batchInterval?: number;
|
||||
/** 最大批量大小,默认 50 */
|
||||
maxBatchSize?: number;
|
||||
/** 是否启用去重,默认 true */
|
||||
enableDeduplication?: boolean;
|
||||
/** 是否合并相同帖子的多次更新,默认 true */
|
||||
enableMerge?: boolean;
|
||||
/** 帖子唯一标识字段,默认 'id' */
|
||||
idField?: string;
|
||||
/** 排序字段,默认 'createdAt' */
|
||||
sortField?: string;
|
||||
/** 变化比例阈值,超过此值触发重置,默认 0.5 */
|
||||
resetThreshold?: number;
|
||||
/** 是否检测部分字段更新 */
|
||||
detectPartialUpdates?: boolean;
|
||||
/** 需要检测变化的字段列表 */
|
||||
trackedFields?: (keyof Post)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认Post差异配置常量
|
||||
*/
|
||||
export const DEFAULT_POST_DIFF_CONFIG: Required<PostDiffConfig> = {
|
||||
batchInterval: 16,
|
||||
maxBatchSize: 50,
|
||||
enableDeduplication: true,
|
||||
enableMerge: true,
|
||||
idField: 'id',
|
||||
sortField: 'createdAt',
|
||||
resetThreshold: 0.5,
|
||||
detectPartialUpdates: true,
|
||||
trackedFields: [
|
||||
'likesCount',
|
||||
'commentsCount',
|
||||
'sharesCount',
|
||||
'viewsCount',
|
||||
'favoritesCount',
|
||||
'isLiked',
|
||||
'isFavorited',
|
||||
'isPinned',
|
||||
'status',
|
||||
'title',
|
||||
'content',
|
||||
'images',
|
||||
'tags',
|
||||
],
|
||||
};
|
||||
|
||||
// ==================== 监听器类型 ====================
|
||||
|
||||
/**
|
||||
* 批量更新监听器类型
|
||||
*/
|
||||
export type PostBatchUpdateListener = (updates: PostUpdate[]) => void;
|
||||
|
||||
/**
|
||||
* Post比较函数类型
|
||||
*/
|
||||
export type PostComparator<T extends PostIdentifier> = (a: T, b: T) => boolean;
|
||||
|
||||
/**
|
||||
* Post排序函数类型
|
||||
*/
|
||||
export type PostSorter<T extends PostIdentifier> = (a: T, b: T) => number;
|
||||
|
||||
// ==================== 统计接口 ====================
|
||||
|
||||
/**
|
||||
* Post差异统计接口
|
||||
*/
|
||||
export interface PostDiffStats {
|
||||
/** 总批次数 */
|
||||
totalBatches: number;
|
||||
/** 总更新数 */
|
||||
totalUpdates: number;
|
||||
/** 平均批量大小 */
|
||||
averageBatchSize: number;
|
||||
/** 上次批次时间 */
|
||||
lastBatchTime: number;
|
||||
/** 新增帖子数 */
|
||||
addedCount: number;
|
||||
/** 更新帖子数 */
|
||||
updatedCount: number;
|
||||
/** 删除帖子数 */
|
||||
deletedCount: number;
|
||||
}
|
||||
75
src/utils/formatTime.ts
Normal file
75
src/utils/formatTime.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
|
||||
const MINUTE_MS = 60 * 1000;
|
||||
const HOUR_MS = 60 * MINUTE_MS;
|
||||
const DAY_MS = 24 * HOUR_MS;
|
||||
const WEEK_MS = 7 * DAY_MS;
|
||||
|
||||
const pad = (num: number) => String(num).padStart(2, '0');
|
||||
const WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
|
||||
export function formatRelativeTime(dateString?: string | null): string {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
|
||||
const now = Date.now();
|
||||
const diffMs = now - date.getTime();
|
||||
|
||||
if (diffMs < 0) return formatDateTime(dateString);
|
||||
if (diffMs < MINUTE_MS) return '刚刚';
|
||||
if (diffMs < HOUR_MS) return `${Math.floor(diffMs / MINUTE_MS)}分钟前`;
|
||||
if (diffMs < DAY_MS) return `${Math.floor(diffMs / HOUR_MS)}小时前`;
|
||||
if (diffMs < 2 * DAY_MS) return `昨天 ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
|
||||
return formatDateTime(dateString);
|
||||
}
|
||||
|
||||
export function formatTime(dateString: string): string {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
|
||||
const now = new Date();
|
||||
const diffInHours = (now.getTime() - date.getTime()) / HOUR_MS;
|
||||
|
||||
if (diffInHours < 24 && date.getDate() === now.getDate()) {
|
||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
const yesterday = new Date(now);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
if (date.getDate() === yesterday.getDate()) return '昨天';
|
||||
|
||||
if (diffInHours < 24 * 7) return WEEKDAYS[date.getDay()];
|
||||
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`;
|
||||
}
|
||||
|
||||
export function formatChatTime(dateString: string): string {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
|
||||
const now = new Date();
|
||||
const diffInHours = (now.getTime() - date.getTime()) / HOUR_MS;
|
||||
|
||||
if (diffInHours < 24 && date.getDate() === now.getDate()) {
|
||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
return formatDistanceToNow(date, { addSuffix: true, locale: zhCN });
|
||||
}
|
||||
|
||||
export function formatDateTime(dateString?: string | null): string {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
export function formatDate(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
}
|
||||
266
src/utils/pagination/types.ts
Normal file
266
src/utils/pagination/types.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* 分页状态管理类型定义
|
||||
*
|
||||
* 提供分页相关的接口和类型定义
|
||||
* 用于统一管理分页状态、缓存和配置
|
||||
*/
|
||||
|
||||
/**
|
||||
* 分页状态
|
||||
* 跟踪特定分页键的分页信息
|
||||
*/
|
||||
export interface PaginationState {
|
||||
/** 当前页码(1-based) */
|
||||
currentPage: number;
|
||||
/** 每页大小 */
|
||||
pageSize: number;
|
||||
/** 已加载的总条目数 */
|
||||
totalLoaded: number;
|
||||
/** 是否存在更多数据 */
|
||||
hasMore: boolean;
|
||||
/** 是否正在加载 */
|
||||
isLoading: boolean;
|
||||
/** 最后加载时间戳 */
|
||||
lastLoadTime: number;
|
||||
/** 分页游标(用于基于游标的分页) */
|
||||
cursor?: string | number | null;
|
||||
/** 是否发生过错误 */
|
||||
hasError: boolean;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面缓存
|
||||
* 存储已加载页面的数据
|
||||
*/
|
||||
export interface PageCache<T = any> {
|
||||
/** 页码 */
|
||||
page: number;
|
||||
/** 该页的数据 */
|
||||
data: T[];
|
||||
/** 缓存时间戳 */
|
||||
cachedAt: number;
|
||||
/** 分页游标 */
|
||||
cursor?: string | number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页配置
|
||||
* 配置分页行为
|
||||
*/
|
||||
export interface PaginationConfig {
|
||||
/** 每页默认大小 */
|
||||
pageSize: number;
|
||||
/** 预加载阈值(距离底部多少条时触发预加载) */
|
||||
prefetchThreshold: number;
|
||||
/** 最大缓存页数 */
|
||||
maxCachedPages: number;
|
||||
/** 缓存过期时间(毫秒) */
|
||||
cacheTtl: number;
|
||||
/** 是否启用预加载 */
|
||||
enablePrefetch: boolean;
|
||||
/** 是否启用缓存 */
|
||||
enableCache: boolean;
|
||||
/** 最大重试次数 */
|
||||
maxRetries: number;
|
||||
/** 重试延迟(毫秒) */
|
||||
retryDelay: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载更多结果
|
||||
* loadMore 方法的返回类型
|
||||
*/
|
||||
export interface LoadMoreResult<T = any> {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 加载的数据 */
|
||||
data: T[];
|
||||
/** 是否有更多数据 */
|
||||
hasMore: boolean;
|
||||
/** 是否来自缓存 */
|
||||
fromCache: boolean;
|
||||
/** 错误信息(如果失败) */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新结果
|
||||
* refresh 方法的返回类型
|
||||
*/
|
||||
export interface RefreshResult<T = any> {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 刷新后的数据 */
|
||||
data: T[];
|
||||
/** 是否有更多数据 */
|
||||
hasMore: boolean;
|
||||
/** 错误信息(如果失败) */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载结果
|
||||
* prefetch 方法的返回类型
|
||||
*/
|
||||
export interface PrefetchResult<T = any> {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 预加载的数据 */
|
||||
data?: T[];
|
||||
/** 错误信息(如果失败) */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页数据加载函数
|
||||
* 用于从数据源加载分页数据
|
||||
*/
|
||||
export type FetchPageFunction<T = any> = (
|
||||
page: number,
|
||||
pageSize: number,
|
||||
cursor?: string | number | null
|
||||
) => Promise<{
|
||||
data: T[];
|
||||
hasMore: boolean;
|
||||
cursor?: string | number | null;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* 默认分页配置
|
||||
*/
|
||||
export const DEFAULT_PAGINATION_CONFIG: PaginationConfig = {
|
||||
pageSize: 20,
|
||||
prefetchThreshold: 5,
|
||||
maxCachedPages: 10,
|
||||
cacheTtl: 5 * 60 * 1000, // 5分钟
|
||||
enablePrefetch: true,
|
||||
enableCache: true,
|
||||
maxRetries: 3,
|
||||
retryDelay: 1000,
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始分页状态
|
||||
*/
|
||||
export function createInitialPaginationState(pageSize: number = 20): PaginationState {
|
||||
return {
|
||||
currentPage: 0,
|
||||
pageSize,
|
||||
totalLoaded: 0,
|
||||
hasMore: true,
|
||||
isLoading: false,
|
||||
lastLoadTime: 0,
|
||||
cursor: null,
|
||||
hasError: false,
|
||||
error: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建页面缓存
|
||||
*/
|
||||
export function createPageCache<T>(
|
||||
page: number,
|
||||
data: T[],
|
||||
cursor?: string | number | null
|
||||
): PageCache<T> {
|
||||
return {
|
||||
page,
|
||||
data,
|
||||
cachedAt: Date.now(),
|
||||
cursor,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否过期
|
||||
*/
|
||||
export function isCacheExpired<T>(cache: PageCache<T>, ttl: number): boolean {
|
||||
return Date.now() - cache.cachedAt > ttl;
|
||||
}
|
||||
|
||||
// ==================== 游标分页相关类型 ====================
|
||||
|
||||
/**
|
||||
* 游标分页方向
|
||||
*/
|
||||
export type CursorDirection = 'forward' | 'backward';
|
||||
|
||||
/**
|
||||
* 游标分页配置
|
||||
*/
|
||||
export interface CursorPaginationConfig {
|
||||
/** 每页数量 */
|
||||
pageSize: number;
|
||||
/** 是否启用双向分页 */
|
||||
bidirectional?: boolean;
|
||||
/** 是否自动加载第一页,默认为 true */
|
||||
autoLoad?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游标分页状态
|
||||
*/
|
||||
export interface CursorPaginationState<T> {
|
||||
/** 数据项列表 */
|
||||
list: T[];
|
||||
/** 下一页游标 */
|
||||
nextCursor: string | null;
|
||||
/** 上一页游标 */
|
||||
prevCursor: string | null;
|
||||
/** 是否有更多数据 */
|
||||
hasMore: boolean;
|
||||
/** 是否正在加载 */
|
||||
isLoading: boolean;
|
||||
/** 是否首屏加载(空列表初始化) */
|
||||
isInitialLoading: boolean;
|
||||
/** 是否正在加载更多 */
|
||||
isLoadingMore: boolean;
|
||||
/** 是否正在刷新 */
|
||||
isRefreshing: boolean;
|
||||
/** 错误信息 */
|
||||
error: string | null;
|
||||
/** 是否为首次加载 */
|
||||
isFirstLoad: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游标分页操作
|
||||
*/
|
||||
export interface CursorPaginationActions<T> {
|
||||
/** 加载更多(下一页) */
|
||||
loadMore: () => Promise<void>;
|
||||
/** 加载上一页(双向分页) */
|
||||
loadPrevious: () => Promise<void>;
|
||||
/** 刷新数据(重新从第一页加载) */
|
||||
refresh: () => Promise<void>;
|
||||
/** 重置状态 */
|
||||
reset: () => void;
|
||||
/** 设置数据(用于外部数据注入) */
|
||||
setList: (list: T[], nextCursor: string | null, prevCursor: string | null, hasMore: boolean) => void;
|
||||
/** 就地更新单条数据 */
|
||||
updateItem: (id: string, updates: Partial<T>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游标分页 Hook 返回值
|
||||
*/
|
||||
export interface UseCursorPaginationReturn<T> extends CursorPaginationState<T>, CursorPaginationActions<T> {}
|
||||
|
||||
/**
|
||||
* 游标分页数据获取函数
|
||||
*/
|
||||
export type CursorFetchFunction<T, P = void> = (params: {
|
||||
cursor?: string;
|
||||
direction: CursorDirection;
|
||||
pageSize: number;
|
||||
/** 额外参数 */
|
||||
extraParams?: P;
|
||||
}) => Promise<{
|
||||
list: T[];
|
||||
next_cursor: string | null;
|
||||
prev_cursor: string | null;
|
||||
has_more: boolean;
|
||||
}>;
|
||||
83
src/utils/platform/domUtils.ts
Normal file
83
src/utils/platform/domUtils.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Web Platform DOM Utilities
|
||||
*
|
||||
* 提供Web平台特定的DOM操作工具函数
|
||||
* 解决跨平台代码中需要访问Web DOM的补丁问题
|
||||
*/
|
||||
|
||||
import { Platform, Keyboard } from 'react-native';
|
||||
|
||||
/**
|
||||
* Blur当前激活的DOM元素(Web)并收起键盘(移动端)
|
||||
*
|
||||
* - Web: blur DOM active element
|
||||
* - iOS/Android: Keyboard.dismiss()
|
||||
*
|
||||
* 替代散落在多个文件中的重复代码
|
||||
*/
|
||||
export function blurActiveElement(): void {
|
||||
if (Platform.OS === 'web') {
|
||||
try {
|
||||
const activeElement = (globalThis as any)?.document?.activeElement as
|
||||
{ blur?: () => void } | undefined;
|
||||
|
||||
if (activeElement?.blur) {
|
||||
activeElement.blur();
|
||||
}
|
||||
} catch {
|
||||
// 安全忽略:某些环境下document可能不可访问
|
||||
}
|
||||
} else {
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前是否有激活的DOM元素(仅Web平台)
|
||||
*/
|
||||
export function hasActiveElement(): boolean {
|
||||
if (Platform.OS !== 'web') return false;
|
||||
|
||||
try {
|
||||
const doc = (globalThis as any)?.document;
|
||||
return !!doc?.activeElement && doc.activeElement !== doc.body;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前激活元素的类型(仅Web平台)
|
||||
* 用于判断是否需要特殊处理(如输入框)
|
||||
*/
|
||||
export function getActiveElementType(): string | null {
|
||||
if (Platform.OS !== 'web') return null;
|
||||
|
||||
try {
|
||||
const activeElement = (globalThis as any)?.document?.activeElement as
|
||||
{ tagName?: string; type?: string } | undefined;
|
||||
|
||||
if (!activeElement) return null;
|
||||
|
||||
const tagName = activeElement.tagName?.toLowerCase();
|
||||
const type = activeElement.type?.toLowerCase();
|
||||
|
||||
return tagName === 'input' ? `input:${type || 'text'}` : tagName || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前激活元素是否为输入类型
|
||||
*/
|
||||
export function isInputFocused(): boolean {
|
||||
const elementType = getActiveElementType();
|
||||
if (!elementType) return false;
|
||||
|
||||
return (
|
||||
elementType.startsWith('input:') ||
|
||||
elementType === 'textarea' ||
|
||||
elementType === 'select'
|
||||
);
|
||||
}
|
||||
12
src/utils/platform/index.ts
Normal file
12
src/utils/platform/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Platform Utilities
|
||||
*
|
||||
* 平台相关的工具函数
|
||||
*/
|
||||
|
||||
export {
|
||||
blurActiveElement,
|
||||
hasActiveElement,
|
||||
getActiveElementType,
|
||||
isInputFocused,
|
||||
} from './domUtils';
|
||||
@@ -1,167 +0,0 @@
|
||||
/**
|
||||
* 响应式样式工具
|
||||
* 提供基于断点的样式生成功能
|
||||
*/
|
||||
|
||||
import { ViewStyle } from 'react-native';
|
||||
import { BREAKPOINTS } from '../presentation/hooks/responsive';
|
||||
|
||||
/**
|
||||
* 断点键名
|
||||
*/
|
||||
export type ResponsiveBreakpoint = 'mobile' | 'tablet' | 'desktop' | 'wide';
|
||||
|
||||
/**
|
||||
* 断点值映射
|
||||
*/
|
||||
export type ResponsiveValues<T> = {
|
||||
mobile?: T;
|
||||
tablet?: T;
|
||||
desktop?: T;
|
||||
wide?: T;
|
||||
};
|
||||
|
||||
/**
|
||||
* 断点样式映射
|
||||
*/
|
||||
export type ResponsiveStyles = {
|
||||
mobile?: ViewStyle;
|
||||
tablet?: ViewStyle;
|
||||
desktop?: ViewStyle;
|
||||
wide?: ViewStyle;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据当前断点返回对应的值
|
||||
*
|
||||
* @param breakpoint - 当前断点
|
||||
* @param values - 各断点对应的值
|
||||
* @returns 对应断点的值,如果未找到则返回 undefined
|
||||
*
|
||||
* @example
|
||||
* const fontSize = responsiveValue('tablet', {
|
||||
* mobile: 14,
|
||||
* tablet: 16,
|
||||
* desktop: 18,
|
||||
* wide: 20
|
||||
* });
|
||||
*/
|
||||
export function responsiveValue<T>(
|
||||
breakpoint: string,
|
||||
values: ResponsiveValues<T>
|
||||
): T | undefined {
|
||||
// 按断点优先级顺序查找
|
||||
const breakpoints: ResponsiveBreakpoint[] = ['wide', 'desktop', 'tablet', 'mobile'];
|
||||
|
||||
for (const bp of breakpoints) {
|
||||
if (breakpoint === bp && values[bp] !== undefined) {
|
||||
return values[bp];
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建响应式样式
|
||||
* 根据当前断点返回对应的样式对象
|
||||
*
|
||||
* @param breakpoint - 当前断点
|
||||
* @param styles - 各断点对应的样式
|
||||
* @returns 合并后的样式对象
|
||||
*
|
||||
* @example
|
||||
* const containerStyle = createResponsiveStyles(breakpoint, {
|
||||
* mobile: { padding: 12 },
|
||||
* tablet: { padding: 24 },
|
||||
* desktop: { padding: 32 },
|
||||
* wide: { padding: 48 }
|
||||
* });
|
||||
*/
|
||||
export function createResponsiveStyles(
|
||||
breakpoint: string,
|
||||
styles: ResponsiveStyles
|
||||
): ViewStyle {
|
||||
const result: ViewStyle = {};
|
||||
|
||||
// 按断点优先级从低到高合并样式
|
||||
const breakpoints: ResponsiveBreakpoint[] = ['mobile', 'tablet', 'desktop', 'wide'];
|
||||
|
||||
for (const bp of breakpoints) {
|
||||
const style = styles[bp];
|
||||
if (style) {
|
||||
Object.assign(result, style);
|
||||
}
|
||||
// 到达当前断点后停止合并
|
||||
if (breakpoint === bp) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取响应式数值
|
||||
* 根据屏幕宽度返回对应的数值
|
||||
*
|
||||
* @param width - 当前屏幕宽度
|
||||
* @param config - 各断点对应的数值
|
||||
* @returns 对应的数值
|
||||
*
|
||||
* @example
|
||||
* const padding = getResponsiveValue(screenWidth, {
|
||||
* mobile: 12,
|
||||
* tablet: 16,
|
||||
* desktop: 24,
|
||||
* wide: 32
|
||||
* });
|
||||
*/
|
||||
export function getResponsiveValue(
|
||||
width: number,
|
||||
config: ResponsiveValues<number>
|
||||
): number | undefined {
|
||||
if (width >= BREAKPOINTS.wide && config.wide !== undefined) {
|
||||
return config.wide;
|
||||
}
|
||||
if (width >= BREAKPOINTS.desktop && config.desktop !== undefined) {
|
||||
return config.desktop;
|
||||
}
|
||||
if (width >= BREAKPOINTS.tablet && config.tablet !== undefined) {
|
||||
return config.tablet;
|
||||
}
|
||||
if (config.mobile !== undefined) {
|
||||
return config.mobile;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前是否为宽屏
|
||||
*
|
||||
* @param width - 当前屏幕宽度
|
||||
* @returns 是否为宽屏(tablet 及以上)
|
||||
*/
|
||||
export function isWideScreen(width: number): boolean {
|
||||
return width >= BREAKPOINTS.tablet;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前是否为桌面端
|
||||
*
|
||||
* @param width - 当前屏幕宽度
|
||||
* @returns 是否为桌面端(desktop 及以上)
|
||||
*/
|
||||
export function isDesktop(width: number): boolean {
|
||||
return width >= BREAKPOINTS.desktop;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前是否为超大屏
|
||||
*
|
||||
* @param width - 当前屏幕宽度
|
||||
* @returns 是否为超大屏(wide)
|
||||
*/
|
||||
export function isWide(width: number): boolean {
|
||||
return width >= BREAKPOINTS.wide;
|
||||
}
|
||||
147
src/utils/sync/types.ts
Normal file
147
src/utils/sync/types.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 同步状态管理类型定义
|
||||
*
|
||||
* 提供连接状态相关的类型、接口和枚举
|
||||
* 用于统一管理 SSE 连接状态
|
||||
*/
|
||||
|
||||
/**
|
||||
* 连接状态枚举
|
||||
* 表示连接生命周期的各个阶段
|
||||
*/
|
||||
export enum ConnectionState {
|
||||
/** 初始状态,未开始连接 */
|
||||
IDLE = 'IDLE',
|
||||
/** 正在建立连接 */
|
||||
CONNECTING = 'CONNECTING',
|
||||
/** 连接已建立 */
|
||||
CONNECTED = 'CONNECTED',
|
||||
/** 连接断开,正在重连 */
|
||||
RECONNECTING = 'RECONNECTING',
|
||||
/** 连接已断开 */
|
||||
DISCONNECTED = 'DISCONNECTED',
|
||||
/** 发生连接错误 */
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接状态信息
|
||||
* 包含完整的状态信息
|
||||
*/
|
||||
export interface ConnectionStateInfo {
|
||||
/** 当前连接状态 */
|
||||
state: ConnectionState;
|
||||
/** 状态变更时间戳 */
|
||||
timestamp: number;
|
||||
/** 错误信息(仅在 ERROR 状态下有效) */
|
||||
error?: Error;
|
||||
/** 当前重连次数(仅在 RECONNECTING 状态下有效) */
|
||||
reconnectAttempts?: number;
|
||||
/** 上次成功连接时间 */
|
||||
lastConnectedAt?: number | null;
|
||||
/** 状态描述(供显示用) */
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接状态监听器类型
|
||||
*/
|
||||
export type ConnectionStateListener = (
|
||||
state: ConnectionStateInfo,
|
||||
previousState: ConnectionStateInfo | null
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* 重连配置接口
|
||||
*/
|
||||
export interface ReconnectConfig {
|
||||
/** 最大重连次数 */
|
||||
maxReconnectAttempts: number;
|
||||
/** 初始重连延迟(毫秒) */
|
||||
initialDelay: number;
|
||||
/** 最大重连延迟(毫秒) */
|
||||
maxDelay: number;
|
||||
/** 延迟倍数(指数退避) */
|
||||
backoffMultiplier: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态转换规则
|
||||
* 定义哪些状态可以转换到哪些状态
|
||||
*/
|
||||
export type StateTransitions = {
|
||||
[key in ConnectionState]?: ConnectionState[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认状态转换规则
|
||||
*/
|
||||
export const DEFAULT_STATE_TRANSITIONS: StateTransitions = {
|
||||
[ConnectionState.IDLE]: [ConnectionState.CONNECTING],
|
||||
[ConnectionState.CONNECTING]: [
|
||||
ConnectionState.CONNECTED,
|
||||
ConnectionState.ERROR,
|
||||
ConnectionState.DISCONNECTED,
|
||||
],
|
||||
[ConnectionState.CONNECTED]: [
|
||||
ConnectionState.DISCONNECTED,
|
||||
ConnectionState.ERROR,
|
||||
ConnectionState.RECONNECTING,
|
||||
],
|
||||
[ConnectionState.RECONNECTING]: [
|
||||
ConnectionState.CONNECTED,
|
||||
ConnectionState.ERROR,
|
||||
ConnectionState.DISCONNECTED,
|
||||
],
|
||||
[ConnectionState.DISCONNECTED]: [
|
||||
ConnectionState.CONNECTING,
|
||||
ConnectionState.RECONNECTING,
|
||||
],
|
||||
[ConnectionState.ERROR]: [
|
||||
ConnectionState.CONNECTING,
|
||||
ConnectionState.RECONNECTING,
|
||||
ConnectionState.DISCONNECTED,
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认重连配置
|
||||
*/
|
||||
export const DEFAULT_RECONNECT_CONFIG: ReconnectConfig = {
|
||||
maxReconnectAttempts: 20,
|
||||
initialDelay: 1000,
|
||||
maxDelay: 30000,
|
||||
backoffMultiplier: 2,
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取状态的中文描述
|
||||
* @param state 连接状态
|
||||
* @param reconnectAttempts 当前重连次数
|
||||
* @param maxReconnectAttempts 最大重连次数
|
||||
* @returns 状态描述字符串
|
||||
*/
|
||||
export function getStateDescription(
|
||||
state: ConnectionState,
|
||||
reconnectAttempts?: number,
|
||||
maxReconnectAttempts?: number
|
||||
): string {
|
||||
switch (state) {
|
||||
case ConnectionState.IDLE:
|
||||
return '未连接';
|
||||
case ConnectionState.CONNECTING:
|
||||
return '正在连接...';
|
||||
case ConnectionState.CONNECTED:
|
||||
return '已连接';
|
||||
case ConnectionState.RECONNECTING:
|
||||
return reconnectAttempts !== undefined && maxReconnectAttempts !== undefined
|
||||
? `正在重连 (${reconnectAttempts}/${maxReconnectAttempts})`
|
||||
: '正在重连...';
|
||||
case ConnectionState.DISCONNECTED:
|
||||
return '已断开连接';
|
||||
case ConnectionState.ERROR:
|
||||
return '连接错误';
|
||||
default:
|
||||
return '未知状态';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user