refactor: restructure core architecture and responsive system
All checks were successful
Frontend CI / ota-android (push) Successful in 1m40s
Frontend CI / ota-ios (push) Successful in 1m39s
Frontend CI / build-and-push-web (push) Successful in 3m30s
Frontend CI / build-android-apk (push) Successful in 1h1m8s

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:
2026-05-05 19:07:33 +08:00
parent f5f9c3a619
commit 3196972596
96 changed files with 4609 additions and 3149 deletions

202
src/utils/cache/types.ts vendored Normal file
View 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
};

View 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);
}

View 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 {
/** 批量处理间隔(毫秒),默认 16ms60fps */
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',
};

View 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
View 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 {
/** 批量处理间隔(毫秒),默认 16ms60fps */
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
View 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' });
}

View 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;
}>;

View 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'
);
}

View File

@@ -0,0 +1,12 @@
/**
* Platform Utilities
*
* 平台相关的工具函数
*/
export {
blurActiveElement,
hasActiveElement,
getActiveElementType,
isInputFocused,
} from './domUtils';

View File

@@ -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
View 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 '未知状态';
}
}