Initial frontend repository commit.

Include app source and update .gitignore to exclude local release artifacts and signing files.

Made-with: Cursor
This commit is contained in:
2026-03-09 21:29:03 +08:00
commit 3968660048
129 changed files with 55599 additions and 0 deletions

309
src/stores/authStore.ts Normal file
View File

@@ -0,0 +1,309 @@
/**
* 认证状态管理
*
* 链路设计:
* authService = 纯 HTTP 层Token 管理),不碰 SQLite
* authStore = 编排层负责API → DB初始化 → 写缓存 → 更新状态
*
* 冷启动问题解法:
* 将 userId 持久化到 AsyncStorage这样 DB 可在 API 调用前提前初始化,
* 彻底打破"需要用户ID才能初始化DB但用户ID来自API"的循环依赖。
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
import { create } from 'zustand';
import { User } from '../types';
import { authService, resolveAuthApiError, websocketService, LoginRequest, RegisterRequest } from '../services';
import {
initDatabase,
closeDatabase,
saveUserCache,
saveCurrentUserCache,
clearCurrentUserCache,
} from '../services/database';
// AsyncStorage 中保存已登录用户 ID 的 key
const USER_ID_KEY = 'auth_user_id';
// ── userId 持久化辅助函数 ──
async function saveUserId(userId: string): Promise<void> {
try {
await AsyncStorage.setItem(USER_ID_KEY, userId);
} catch (err) {
console.warn('[AuthStore] 保存 userId 失败:', err);
}
}
async function loadUserId(): Promise<string | null> {
try {
return await AsyncStorage.getItem(USER_ID_KEY);
} catch {
return null;
}
}
async function clearUserId(): Promise<void> {
try {
await AsyncStorage.removeItem(USER_ID_KEY);
} catch {}
}
// ── 写用户缓存DB 已就绪后调用)──
async function cacheUser(user: User): Promise<void> {
try {
await saveUserCache(user);
await saveCurrentUserCache(user);
} catch (err) {
console.warn('[AuthStore] 写用户缓存失败:', err);
}
}
// ── 将 API 错误转换为用户可读的中文提示 ──
function resolveLoginError(error: any): string {
const code: number = error?.code ?? 0;
const msg: string = error?.message ?? '';
// 网络层错误(无法连接服务器)
if (
msg.includes('Network request failed') ||
msg.includes('网络请求失败') ||
msg.includes('Failed to fetch') ||
code === 500
) {
return '无法连接到服务器,请检查网络连接后重试';
}
// 账号或密码错误
if (code === 401 || msg.includes('密码') || msg.includes('用户名') || msg.includes('incorrect')) {
return '用户名或密码错误,请重新输入';
}
// 账号被禁用/封禁
if (code === 403 || msg.includes('禁止') || msg.includes('封禁') || msg.includes('forbidden')) {
return '账号已被禁用,请联系管理员';
}
// 服务端明确返回了 message直接用
if (msg && msg !== '登录失败') {
return msg;
}
return '登录失败,请稍后重试';
}
// ── 启动 WebSocket 服务 ──
async function startWebSocket(): Promise<void> {
try {
await websocketService.start();
console.log('[AuthStore] WebSocket 服务启动成功');
} catch (error) {
console.error('[AuthStore] 启动 WebSocket 服务失败:', error);
}
}
interface AuthState {
isAuthenticated: boolean;
currentUser: User | null;
isLoading: boolean;
error: string | null;
login: (credentials: LoginRequest) => Promise<boolean>;
register: (data: RegisterRequest) => Promise<boolean>;
logout: () => Promise<void>;
updateUser: (user: Partial<User>) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
fetchCurrentUser: () => Promise<void>;
}
export const useAuthStore = create<AuthState>((set) => ({
isAuthenticated: false,
currentUser: null,
isLoading: false,
error: null,
// ── 登录 ──
// 流程API → 保存Token → 初始化DB → 持久化userId → 写缓存 → 设置状态
login: async (credentials: LoginRequest) => {
set({ isLoading: true, error: null });
try {
// 1. API 登录authService 只负责 HTTP + Token
const response = await authService.login(credentials);
const user = response.user;
if (!user?.id) {
throw new Error('登录响应中缺少用户信息');
}
const userId = String(user.id);
// 2. 初始化用户专属数据库
await initDatabase(userId);
// 3. 持久化 userId供下次冷启动提前初始化 DB 用)
await saveUserId(userId);
// 4. 写用户缓存DB 已就绪)
await cacheUser(user);
set({
isAuthenticated: true,
currentUser: user,
isLoading: false,
error: null,
});
// 5. 启动 WebSocket
await startWebSocket();
return true;
} catch (error: any) {
set({
isLoading: false,
error: resolveLoginError(error),
});
return false;
}
},
// ── 注册 ──
register: async (data: RegisterRequest) => {
set({ isLoading: true, error: null });
try {
const response = await authService.register(data);
const user = response.user;
if (!user?.id) {
throw new Error('注册响应中缺少用户信息');
}
const userId = String(user.id);
await initDatabase(userId);
await saveUserId(userId);
await cacheUser(user);
set({
isAuthenticated: true,
currentUser: user,
isLoading: false,
error: null,
});
await startWebSocket();
return true;
} catch (error: any) {
set({
isLoading: false,
error: resolveAuthApiError(error, '注册失败,请稍后重试'),
});
return false;
}
},
// ── 登出 ──
logout: async () => {
set({ isLoading: true });
try {
// 1. 通知服务端Token 清理在 authService 内部完成)
await authService.logout();
// 2. 停止 WebSocket
websocketService.stop();
// 3. 清除 DB 中的用户缓存DB 此时一定已初始化)
await clearCurrentUserCache().catch(() => {});
// 4. 关闭数据库连接
await closeDatabase();
// 5. 清除持久化的 userId
await clearUserId();
} catch (error) {
console.error('[AuthStore] 登出失败:', error);
} finally {
set({
isAuthenticated: false,
currentUser: null,
isLoading: false,
error: null,
});
}
},
// ── 冷启动校验 ──
// 流程读取已保存的userId → 提前初始化DB → 纯API验证Token → 更新缓存 → 设置状态
fetchCurrentUser: async () => {
set({ isLoading: true });
try {
// 1. 从 AsyncStorage 读取上次登录保存的 userId
// 这是关键:让 DB 在 API 调用前就能初始化,打破循环依赖
const savedUserId = await loadUserId();
if (savedUserId) {
// 2. 用已知的 userId 提前初始化 DB
try {
await initDatabase(savedUserId);
} catch (dbErr) {
console.warn('[AuthStore] DB 预初始化失败:', dbErr);
}
}
// 3. 纯 API 调用验证 Token 有效性fetchCurrentUserFromAPI 完全不碰 DB
const user = await authService.fetchCurrentUserFromAPI();
if (user) {
const userId = String(user.id);
// 4. 若 userId 发生变化(极少数情况),重新初始化 DB 并更新持久化
if (!savedUserId || savedUserId !== userId) {
await initDatabase(userId);
await saveUserId(userId);
}
// 5. DB 已就绪,更新用户缓存
await cacheUser(user);
set({
isAuthenticated: true,
currentUser: user,
isLoading: false,
});
// 6. 启动 WebSocket
await startWebSocket();
} else {
// Token 已失效或不存在
await clearUserId();
set({
isAuthenticated: false,
currentUser: null,
isLoading: false,
});
}
} catch (error) {
console.error('[AuthStore] fetchCurrentUser 异常:', error);
set({
isAuthenticated: false,
currentUser: null,
isLoading: false,
error: '获取用户信息失败',
});
}
},
updateUser: (userData: Partial<User>) =>
set((state) => ({
currentUser: state.currentUser ? { ...state.currentUser, ...userData } : null,
})),
setLoading: (loading: boolean) => set({ isLoading: loading }),
setError: (error: string | null) => set({ error }),
}));
// 导出 selector hooks 以优化性能
export const useCurrentUser = () => useAuthStore((state) => state.currentUser);
export const useIsAuthenticated = () => useAuthStore((state) => state.isAuthenticated);
export const useAuthLoading = () => useAuthStore((state) => state.isLoading);

52
src/stores/cacheBus.ts Normal file
View File

@@ -0,0 +1,52 @@
export type CacheEventType =
| 'list_updated'
| 'detail_updated'
| 'cache_state_updated'
| 'error';
export interface CacheEvent<TPayload = any> {
type: CacheEventType;
payload: TPayload;
timestamp: number;
}
export type CacheSubscriber<TEvent extends CacheEvent = CacheEvent> = (event: TEvent) => void;
/**
* 统一缓存总线基座:
* - 单一订阅入口
* - 统一事件通知
* - 新订阅立即收到快照
*/
export abstract class CacheBus<TSnapshot, TEvent extends CacheEvent = CacheEvent> {
private subscribers = new Set<CacheSubscriber<TEvent>>();
protected abstract getSnapshot(): TSnapshot;
protected notify(event: TEvent): void {
this.subscribers.forEach((subscriber) => {
try {
subscriber(event);
} catch (error) {
console.error('[CacheBus] subscriber error:', error);
}
});
}
subscribe(subscriber: CacheSubscriber<TEvent>): () => void {
this.subscribers.add(subscriber);
this.emitInitialSnapshot(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
}
protected emitInitialSnapshot(subscriber: CacheSubscriber<TEvent>): void {
subscriber({
type: 'cache_state_updated',
payload: this.getSnapshot(),
timestamp: Date.now(),
} as TEvent);
}
}

323
src/stores/groupManager.ts Normal file
View File

@@ -0,0 +1,323 @@
import {
GroupListResponse,
GroupMemberListResponse,
GroupMemberResponse,
GroupResponse,
} from '../types/dto';
import { groupService } from '../services/groupService';
import {
getAllGroupsCache,
getGroupCache,
getGroupMembersCache,
saveGroupCache,
saveGroupMembersCache,
saveGroupsCache,
saveUsersCache,
} from '../services/database';
import { CacheBus, CacheEvent } from './cacheBus';
interface GroupManagerSnapshot {
groupIds: string[];
memberGroupIds: string[];
}
type GroupManagerEvent =
| CacheEvent<{ groups: GroupResponse[] }>
| CacheEvent<{ groupId: string; group: GroupResponse | null }>
| CacheEvent<{ groupId: string; members: GroupMemberResponse[] }>
| CacheEvent<GroupManagerSnapshot>
| CacheEvent<{ error: unknown; context: string }>;
const GROUP_TTL = 60 * 1000;
const MEMBERS_TTL = 120 * 1000;
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
class GroupManager extends CacheBus<GroupManagerSnapshot, GroupManagerEvent> {
private groupsListCache: CacheEntry<GroupResponse[]> | null = null;
private groupDetailCache = new Map<string, CacheEntry<GroupResponse | null>>();
private groupMembersCache = new Map<string, CacheEntry<GroupMemberResponse[]>>();
private pendingRequests = new Map<string, Promise<any>>();
protected getSnapshot(): GroupManagerSnapshot {
return {
groupIds: [...this.groupDetailCache.keys()],
memberGroupIds: [...this.groupMembersCache.keys()],
};
}
private isExpired(entry: CacheEntry<any>): boolean {
return Date.now() - entry.timestamp > entry.ttl;
}
private async dedupe<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const pending = this.pendingRequests.get(key) as Promise<T> | undefined;
if (pending) return pending;
const request = fetcher().finally(() => {
this.pendingRequests.delete(key);
});
this.pendingRequests.set(key, request);
return request;
}
async getGroups(page = 1, pageSize = 20, forceRefresh = false): Promise<GroupListResponse> {
if (page === 1 && !forceRefresh && this.groupsListCache && !this.isExpired(this.groupsListCache)) {
return {
list: this.groupsListCache.data,
total: this.groupsListCache.data.length,
page: 1,
page_size: pageSize,
total_pages: 1,
};
}
if (page === 1 && !forceRefresh && this.groupsListCache && this.isExpired(this.groupsListCache)) {
this.refreshGroupsInBackground(page, pageSize);
return {
list: this.groupsListCache.data,
total: this.groupsListCache.data.length,
page: 1,
page_size: pageSize,
total_pages: 1,
};
}
if (page === 1 && !forceRefresh) {
const localGroups = await getAllGroupsCache();
if (localGroups.length > 0) {
this.groupsListCache = { data: localGroups, timestamp: Date.now(), ttl: GROUP_TTL };
this.notify({
type: 'list_updated',
payload: { groups: localGroups },
timestamp: Date.now(),
});
this.refreshGroupsInBackground(page, pageSize);
return {
list: localGroups,
total: localGroups.length,
page: 1,
page_size: pageSize,
total_pages: 1,
};
}
}
return this.dedupe(`groups:list:${page}:${pageSize}`, async () => {
const response = await groupService.fetchGroupsFromApi(page, pageSize);
const groups = response.list || [];
if (page === 1) {
this.groupsListCache = { data: groups, timestamp: Date.now(), ttl: GROUP_TTL };
}
await saveGroupsCache(groups).catch(() => {});
this.notify({
type: 'list_updated',
payload: { groups },
timestamp: Date.now(),
});
return response;
});
}
private refreshGroupsInBackground(page = 1, pageSize = 20): void {
this.dedupe(`groups:list:bg:${page}:${pageSize}`, async () => {
const response = await groupService.fetchGroupsFromApi(page, pageSize);
const groups = response.list || [];
if (page === 1) {
this.groupsListCache = { data: groups, timestamp: Date.now(), ttl: GROUP_TTL };
saveGroupsCache(groups).catch(() => {});
}
this.notify({
type: 'list_updated',
payload: { groups },
timestamp: Date.now(),
});
return response;
}).catch((error) => {
this.notify({
type: 'error',
payload: { error, context: 'refreshGroupsInBackground' },
timestamp: Date.now(),
});
});
}
async getGroup(groupId: number | string, forceRefresh = false): Promise<GroupResponse> {
const id = String(groupId);
const cached = this.groupDetailCache.get(id);
if (!forceRefresh && cached && !this.isExpired(cached) && cached.data) {
return cached.data;
}
if (!forceRefresh && cached && this.isExpired(cached) && cached.data) {
this.refreshGroupInBackground(id);
return cached.data;
}
if (!forceRefresh) {
const localGroup = await getGroupCache(id);
if (localGroup) {
this.groupDetailCache.set(id, { data: localGroup, timestamp: Date.now(), ttl: GROUP_TTL });
this.notify({
type: 'detail_updated',
payload: { groupId: id, group: localGroup },
timestamp: Date.now(),
});
this.refreshGroupInBackground(id);
return localGroup;
}
}
return this.dedupe(`groups:detail:${id}`, async () => {
const group = await groupService.fetchGroupFromApi(id);
this.groupDetailCache.set(id, { data: group, timestamp: Date.now(), ttl: GROUP_TTL });
await saveGroupCache(group).catch(() => {});
this.notify({
type: 'detail_updated',
payload: { groupId: id, group },
timestamp: Date.now(),
});
return group;
});
}
private refreshGroupInBackground(groupId: string): void {
this.dedupe(`groups:detail:bg:${groupId}`, async () => {
const group = await groupService.fetchGroupFromApi(groupId);
this.groupDetailCache.set(groupId, { data: group, timestamp: Date.now(), ttl: GROUP_TTL });
saveGroupCache(group).catch(() => {});
this.notify({
type: 'detail_updated',
payload: { groupId, group },
timestamp: Date.now(),
});
return group;
}).catch((error) => {
this.notify({
type: 'error',
payload: { error, context: 'refreshGroupInBackground' },
timestamp: Date.now(),
});
});
}
async getMembers(groupId: number | string, page = 1, pageSize = 50, forceRefresh = false): Promise<GroupMemberListResponse> {
const id = String(groupId);
const key = `${id}:${page}:${pageSize}`;
const cached = this.groupMembersCache.get(key);
if (!forceRefresh && cached && !this.isExpired(cached)) {
return {
list: cached.data,
total: cached.data.length,
page,
page_size: pageSize,
total_pages: 1,
};
}
if (!forceRefresh && cached && this.isExpired(cached)) {
this.refreshMembersInBackground(id, page, pageSize);
return {
list: cached.data,
total: cached.data.length,
page,
page_size: pageSize,
total_pages: 1,
};
}
if (page === 1 && !forceRefresh) {
const localMembers = await getGroupMembersCache(id);
if (localMembers.length > 0) {
this.groupMembersCache.set(key, {
data: localMembers,
timestamp: Date.now(),
ttl: MEMBERS_TTL,
});
this.notify({
type: 'detail_updated',
payload: { groupId: id, members: localMembers },
timestamp: Date.now(),
});
this.refreshMembersInBackground(id, page, pageSize);
return {
list: localMembers,
total: localMembers.length,
page,
page_size: pageSize,
total_pages: 1,
};
}
}
return this.dedupe(`groups:members:${key}`, async () => {
const response = await groupService.fetchMembersFromApi(id, page, pageSize);
const members = response.list || [];
this.groupMembersCache.set(key, { data: members, timestamp: Date.now(), ttl: MEMBERS_TTL });
if (page === 1) {
await saveGroupMembersCache(id, members).catch(() => {});
}
await saveUsersCache(
members
.map((member) => member.user)
.filter((user): user is NonNullable<typeof user> => Boolean(user))
).catch(() => {});
this.notify({
type: 'detail_updated',
payload: { groupId: id, members },
timestamp: Date.now(),
});
return response;
});
}
private refreshMembersInBackground(groupId: string, page = 1, pageSize = 50): void {
this.dedupe(`groups:members:bg:${groupId}:${page}:${pageSize}`, async () => {
const response = await groupService.fetchMembersFromApi(groupId, page, pageSize);
const members = response.list || [];
const key = `${groupId}:${page}:${pageSize}`;
this.groupMembersCache.set(key, { data: members, timestamp: Date.now(), ttl: MEMBERS_TTL });
if (page === 1) {
saveGroupMembersCache(groupId, members).catch(() => {});
}
saveUsersCache(
members
.map((member) => member.user)
.filter((user): user is NonNullable<typeof user> => Boolean(user))
).catch(() => {});
this.notify({
type: 'detail_updated',
payload: { groupId, members },
timestamp: Date.now(),
});
return response;
}).catch((error) => {
this.notify({
type: 'error',
payload: { error, context: 'refreshMembersInBackground' },
timestamp: Date.now(),
});
});
}
invalidate(groupId?: string): void {
if (!groupId) {
this.groupsListCache = null;
this.groupDetailCache.clear();
this.groupMembersCache.clear();
return;
}
this.groupDetailCache.delete(groupId);
[...this.groupMembersCache.keys()].forEach((key) => {
if (key.startsWith(`${groupId}:`)) {
this.groupMembersCache.delete(key);
}
});
}
}
export const groupManager = new GroupManager();

38
src/stores/index.ts Normal file
View File

@@ -0,0 +1,38 @@
/**
* 状态管理 Store 导出
*/
export { useAuthStore, useCurrentUser, useIsAuthenticated, useAuthLoading } from './authStore';
export {
useUserStore,
usePosts,
useNotifications,
useNotificationBadge,
useMessageUnreadCount,
useSearchHistory
} from './userStore';
// MessageManager 新架构导出(已替换 conversationStore
export { messageManager } from './messageManager';
export type { MessageEvent, MessageEventType, MessageSubscriber } from './messageManager';
export { postManager } from './postManager';
export { groupManager } from './groupManager';
export { userManager } from './userManager';
export {
useConversations as useMessageManagerConversations,
useMessages,
useSendMessage,
useMarkAsRead,
useUnreadCount,
useTotalUnreadCount,
useConversation as useMessageManagerConversation,
useMessageManager,
useChat,
useMessageList,
useMessageListRefresh,
useCreateConversation,
useUpdateConversation,
useSystemUnreadCount as useMessageManagerSystemUnreadCount,
useGroupTyping,
useGroupMuted,
} from './messageManagerHooks';

2254
src/stores/messageManager.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,697 @@
/**
* MessageManager React Hooks
*
* 提供React组件与MessageManager的集成
* 所有hooks都基于MessageManager的订阅机制
* 确保组件能实时获取状态更新
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import { ConversationResponse, MessageResponse, MessageSegment } from '../types/dto';
import { messageManager, MessageEvent } from './messageManager';
// ==================== useConversations - 获取会话列表 ====================
interface UseConversationsReturn {
conversations: ConversationResponse[];
isLoading: boolean;
refresh: () => Promise<void>;
}
/**
* 获取会话列表
* 用于MessageListScreen等需要显示会话列表的组件
*/
export function useConversations(): UseConversationsReturn {
const [conversations, setConversations] = useState<ConversationResponse[]>(
() => messageManager.getConversations()
);
const [isLoading, setIsLoading] = useState(() => messageManager.isLoading());
useEffect(() => {
// 订阅MessageManager的事件
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
switch (event.type) {
case 'conversations_updated':
setConversations(messageManager.getConversations());
break;
case 'connection_changed':
// 连接状态变化时可能需要刷新
break;
}
});
// 初始化时确保 MessageManager 已初始化
messageManager.initialize().catch(error => {
console.error('[useConversations] 初始化失败:', error);
});
// 冷启动兜底:延迟做一次强制刷新,避免首次因时序问题拿到空列表
const coldStartSyncTimer = setTimeout(() => {
messageManager.fetchConversations(true).catch(error => {
console.error('[useConversations] 冷启动强制刷新失败:', error);
});
}, 1200);
return () => {
clearTimeout(coldStartSyncTimer);
unsubscribe();
};
}, []);
const refresh = useCallback(async () => {
setIsLoading(true);
await messageManager.fetchConversations(true);
setIsLoading(false);
}, []);
return {
conversations,
isLoading,
refresh,
};
}
// ==================== useMessages - 获取指定会话的消息 ====================
interface UseMessagesReturn {
messages: MessageResponse[];
isLoading: boolean;
hasMore: boolean;
loadMore: () => Promise<void>;
refresh: () => Promise<void>;
}
/**
* 获取指定会话的消息
* 用于ChatScreen等需要显示消息的组件
*
* @param conversationId 会话ID
*/
export function useMessages(conversationId: string | null): UseMessagesReturn {
const [messages, setMessages] = useState<MessageResponse[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
useEffect(() => {
if (!conversationId) {
setMessages([]);
return;
}
const normalizedConversationId = String(conversationId);
console.log('[useMessages][DEBUG] useEffect 开始执行:', { conversationId: normalizedConversationId, timestamp: Date.now() });
setIsLoading(true);
setHasMore(true);
let isUnmounted = false;
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
if (
event.type === 'messages_updated' &&
String(event.payload.conversationId) === normalizedConversationId
) {
setMessages(event.payload.messages);
if (!isUnmounted) {
setIsLoading(false);
}
}
});
// 先读取内存快照,确保页面切换时无闪烁
const cachedMessages = messageManager.getMessages(normalizedConversationId);
if (cachedMessages.length > 0) {
setMessages(cachedMessages);
}
// 架构入口:激活会话并完成初始化/同步
messageManager.activateConversation(normalizedConversationId).catch(error => {
console.error('[useMessages] 激活会话失败:', error);
if (!isUnmounted) {
setIsLoading(false);
}
}).finally(() => {
if (!isUnmounted) {
setIsLoading(false);
}
});
return () => {
isUnmounted = true;
unsubscribe();
if (String(messageManager.getActiveConversation()) === normalizedConversationId) {
messageManager.setActiveConversation(null);
}
};
}, [conversationId]);
const loadMore = useCallback(async () => {
if (!conversationId || isLoading || !hasMore) return;
const currentMessages = messageManager.getMessages(conversationId);
if (currentMessages.length === 0) return;
// 获取最早的消息seq
const minSeq = Math.min(...currentMessages.map(m => m.seq));
setIsLoading(true);
const loadedMessages = await messageManager.loadMoreMessages(conversationId, minSeq, 20);
setIsLoading(false);
// 如果没有加载到新消息,说明没有更多了
if (loadedMessages.length === 0) {
setHasMore(false);
}
}, [conversationId, isLoading, hasMore]);
const refresh = useCallback(async () => {
if (!conversationId) return;
setIsLoading(true);
await messageManager.fetchMessages(conversationId);
setMessages(messageManager.getMessages(conversationId));
setIsLoading(false);
setHasMore(true);
}, [conversationId]);
return {
messages,
isLoading,
hasMore,
loadMore,
refresh,
};
}
// ==================== useSendMessage - 发送消息 ====================
interface UseSendMessageReturn {
sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | null>;
isSending: boolean;
}
/**
* 发送消息
* 返回发送函数和发送状态
*/
export function useSendMessage(conversationId: string | null): UseSendMessageReturn {
const [isSending, setIsSending] = useState(false);
const sendMessage = useCallback(
async (segments: MessageSegment[], options?: { replyToId?: string }): Promise<MessageResponse | null> => {
if (!conversationId) return null;
setIsSending(true);
try {
const message = await messageManager.sendMessage(conversationId, segments, options);
return message;
} finally {
setIsSending(false);
}
},
[conversationId]
);
return {
sendMessage,
isSending,
};
}
// ==================== useMarkAsRead - 标记已读 ====================
interface UseMarkAsReadReturn {
markAsRead: (seq: number) => Promise<void>;
markAllAsRead: () => Promise<void>;
isMarking: boolean;
}
/**
* 标记已读
* 用于ChatScreen标记当前会话已读
*/
export function useMarkAsRead(conversationId: string | null): UseMarkAsReadReturn {
const [isMarking, setIsMarking] = useState(false);
const markAsRead = useCallback(
async (seq: number) => {
if (!conversationId) return;
setIsMarking(true);
try {
await messageManager.markAsRead(conversationId, seq);
} finally {
setIsMarking(false);
}
},
[conversationId]
);
const markAllAsRead = useCallback(async () => {
setIsMarking(true);
try {
await messageManager.markAllAsRead();
} finally {
setIsMarking(false);
}
}, []);
return {
markAsRead,
markAllAsRead,
isMarking,
};
}
// ==================== useUnreadCount - 获取未读数 ====================
interface UseUnreadCountReturn {
totalUnreadCount: number;
systemUnreadCount: number;
}
/**
* 获取未读消息数
* 用于TabBar角标等
*/
export function useUnreadCount(): UseUnreadCountReturn {
const [counts, setCounts] = useState(() => messageManager.getUnreadCount());
useEffect(() => {
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
if (event.type === 'unread_count_updated') {
setCounts(messageManager.getUnreadCount());
}
});
// 初始化时获取最新未读数
messageManager.fetchUnreadCount();
return unsubscribe;
}, []);
return {
totalUnreadCount: counts.total,
systemUnreadCount: counts.system,
};
}
// ==================== useTotalUnreadCount - 获取总未读数(包含系统消息) ====================
/**
* 获取总未读消息数(会话未读 + 系统消息未读)
* 专用于TabBar徽章显示
*/
export function useTotalUnreadCount(): number {
const [totalCount, setTotalCount] = useState(() => {
const counts = messageManager.getUnreadCount();
return counts.total + counts.system;
});
useEffect(() => {
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
if (event.type === 'unread_count_updated') {
const counts = messageManager.getUnreadCount();
setTotalCount(counts.total + counts.system);
}
});
// 初始化时获取最新未读数
messageManager.fetchUnreadCount();
return unsubscribe;
}, []);
return totalCount;
}
// ==================== useConversation - 获取单个会话信息 ====================
interface UseConversationReturn {
conversation: ConversationResponse | null;
isLoading: boolean;
refresh: () => Promise<void>;
}
/**
* 获取单个会话信息
*/
export function useConversation(conversationId: string | null): UseConversationReturn {
const [conversation, setConversation] = useState<ConversationResponse | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (!conversationId) {
setConversation(null);
return;
}
// 获取当前会话信息
setConversation(messageManager.getConversation(conversationId));
// 订阅更新
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
if (event.type === 'conversations_updated') {
const updated = messageManager.getConversation(conversationId);
setConversation(updated);
}
});
return unsubscribe;
}, [conversationId]);
const refresh = useCallback(async () => {
if (!conversationId) return;
setIsLoading(true);
await messageManager.fetchConversationDetail(conversationId);
setConversation(messageManager.getConversation(conversationId));
setIsLoading(false);
}, [conversationId]);
return {
conversation,
isLoading,
refresh,
};
}
// ==================== useMessageManager - 通用订阅 ====================
interface UseMessageManagerReturn {
isConnected: boolean;
isInitialized: boolean;
}
/**
* 通用MessageManager订阅
* 用于获取连接状态等全局信息
*/
export function useMessageManager(): UseMessageManagerReturn {
const [isConnected, setIsConnected] = useState(() => messageManager.isConnected());
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
if (event.type === 'connection_changed') {
setIsConnected(event.payload.connected);
}
});
// 初始化
messageManager.initialize().then(() => {
setIsInitialized(true);
});
return unsubscribe;
}, []);
return {
isConnected,
isInitialized,
};
}
// ==================== useCreateConversation - 创建会话 ====================
interface UseCreateConversationReturn {
createConversation: (userId: string) => Promise<ConversationResponse | null>;
isCreating: boolean;
}
/**
* 创建私聊会话
*/
export function useCreateConversation(): UseCreateConversationReturn {
const [isCreating, setIsCreating] = useState(false);
const createConversation = useCallback(async (userId: string): Promise<ConversationResponse | null> => {
setIsCreating(true);
try {
const conversation = await messageManager.createConversation(userId);
return conversation;
} finally {
setIsCreating(false);
}
}, []);
return {
createConversation,
isCreating,
};
}
// ==================== useUpdateConversation - 更新会话 ====================
interface UseUpdateConversationReturn {
updateConversation: (conversationId: string, updates: Partial<ConversationResponse>) => void;
}
/**
* 本地更新会话信息
*/
export function useUpdateConversation(): UseUpdateConversationReturn {
const updateConversation = useCallback((conversationId: string, updates: Partial<ConversationResponse>) => {
messageManager.updateConversation(conversationId, updates);
}, []);
return {
updateConversation,
};
}
// ==================== useSystemUnreadCount - 系统消息未读数操作 ====================
interface UseSystemUnreadCountReturn {
setSystemUnreadCount: (count: number) => void;
incrementSystemUnreadCount: () => void;
decrementSystemUnreadCount: (count?: number) => void;
}
/**
* 系统消息未读数操作
*/
export function useSystemUnreadCount(): UseSystemUnreadCountReturn {
const setSystemUnreadCount = useCallback((count: number) => {
messageManager.setSystemUnreadCount(count);
}, []);
const incrementSystemUnreadCount = useCallback(() => {
messageManager.incrementSystemUnreadCount();
}, []);
const decrementSystemUnreadCount = useCallback((count?: number) => {
messageManager.decrementSystemUnreadCount(count);
}, []);
return {
setSystemUnreadCount,
incrementSystemUnreadCount,
decrementSystemUnreadCount,
};
}
// ==================== useMessageListRefresh - MessageListScreen焦点刷新 ====================
interface UseMessageListRefreshReturn {
refresh: () => Promise<void>;
isRefreshing: boolean;
}
/**
* MessageListScreen焦点刷新Hook
* 从 ChatScreen 返回时MessageManager 内存状态已经是最新的markAsRead 乐观更新),
* 无需再请求服务器(避免时序竞争覆盖已读状态)。
* 仅在用户主动下拉刷新时才触发服务器请求。
* @deprecated isFocused 参数保留仅为向后兼容,不再触发焦点刷新
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function useMessageListRefresh(_isFocused?: boolean): UseMessageListRefreshReturn {
const [isRefreshing, setIsRefreshing] = useState(false);
const lastRefreshTimeRef = useRef<number>(0);
const refresh = useCallback(async () => {
// 防止重复刷新最小间隔500ms
const now = Date.now();
if (now - lastRefreshTimeRef.current < 500) {
return;
}
lastRefreshTimeRef.current = now;
setIsRefreshing(true);
try {
await messageManager.fetchConversations(true);
} catch (error) {
console.error('[useMessageListRefresh] 刷新失败:', error);
} finally {
setIsRefreshing(false);
}
}, []);
// 获得焦点时不主动拉服务器——内存状态已是最新,避免时序竞争
// refresh 仅供下拉刷新手动调用
return {
refresh,
isRefreshing,
};
}
// ==================== 便捷hooks组合 ====================
/**
* ChatScreen专用Hook组合
* 整合所有ChatScreen需要的功能
*/
interface UseChatReturn {
// 消息相关
messages: MessageResponse[];
isLoadingMessages: boolean;
hasMoreMessages: boolean;
loadMoreMessages: () => Promise<void>;
refreshMessages: () => Promise<void>;
// 发送消息
sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | null>;
isSending: boolean;
// 已读相关
markAsRead: (seq: number) => Promise<void>;
// 会话信息
conversation: ConversationResponse | null;
}
export function useChat(conversationId: string | null): UseChatReturn {
const { messages, isLoading: isLoadingMessages, hasMore: hasMoreMessages, loadMore, refresh } = useMessages(conversationId);
const { sendMessage, isSending } = useSendMessage(conversationId);
const { markAsRead } = useMarkAsRead(conversationId);
const { conversation } = useConversation(conversationId);
return {
messages,
isLoadingMessages,
hasMoreMessages,
loadMoreMessages: loadMore,
refreshMessages: refresh,
sendMessage,
isSending,
markAsRead,
conversation,
};
}
// ==================== useGroupTyping - 群聊输入状态 ====================
interface UseGroupTypingReturn {
typingUsers: string[];
}
/**
* 获取群聊输入状态
* @param groupId 群组ID
*/
export function useGroupTyping(groupId: string | null): UseGroupTypingReturn {
const [typingUsers, setTypingUsers] = useState<string[]>([]);
useEffect(() => {
if (!groupId) {
setTypingUsers([]);
return;
}
// 初始值
setTypingUsers(messageManager.getTypingUsers(groupId));
// 订阅输入状态变化
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
if (event.type === 'typing_status' && event.payload.groupId === groupId) {
setTypingUsers(event.payload.typingUsers);
}
});
return unsubscribe;
}, [groupId]);
return { typingUsers };
}
// ==================== useGroupMuted - 群聊禁言状态 ====================
interface UseGroupMutedReturn {
isMuted: boolean;
setMuted: (muted: boolean) => void;
}
/**
* 获取群聊禁言状态
* @param groupId 群组ID
* @param currentUserId 当前用户ID用于判断禁言通知是否针对当前用户
*/
export function useGroupMuted(groupId: string | null, currentUserId?: string): UseGroupMutedReturn {
const [isMuted, setIsMuted] = useState(false);
useEffect(() => {
if (!groupId) {
setIsMuted(false);
return;
}
// 初始值
setIsMuted(messageManager.isMuted(groupId));
// 订阅群通知变化
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
if (event.type === 'group_notice' && event.payload.groupId === groupId) {
const { noticeType, data } = event.payload;
// 如果提供了当前用户ID只响应当前用户的禁言/解禁通知
if ((noticeType === 'muted' || noticeType === 'unmuted')) {
if (!currentUserId || data?.user_id === currentUserId) {
setIsMuted(noticeType === 'muted');
}
}
}
});
return unsubscribe;
}, [groupId, currentUserId]);
const setMuted = useCallback((muted: boolean) => {
if (groupId) {
messageManager.setMutedStatus(groupId, muted);
setIsMuted(muted);
}
}, [groupId]);
return { isMuted, setMuted };
}
/**
* MessageListScreen专用Hook组合
*/
interface UseMessageListReturn {
conversations: ConversationResponse[];
isLoading: boolean;
refresh: () => Promise<void>;
totalUnreadCount: number;
systemUnreadCount: number;
markAllAsRead: () => Promise<void>;
isMarking: boolean;
}
export function useMessageList(): UseMessageListReturn {
const { conversations, isLoading, refresh } = useConversations();
const { totalUnreadCount, systemUnreadCount } = useUnreadCount();
const { markAllAsRead, isMarking } = useMarkAsRead(null);
return {
conversations,
isLoading,
refresh,
totalUnreadCount,
systemUnreadCount,
markAllAsRead,
isMarking,
};
}

174
src/stores/postManager.ts Normal file
View File

@@ -0,0 +1,174 @@
import { Post } from '../types';
import { postService } from '../services/postService';
import { CacheBus, CacheEvent } from './cacheBus';
interface PostListPayload {
key: string;
posts: Post[];
}
interface PostDetailPayload {
postId: string;
post: Post | null;
}
interface PostManagerSnapshot {
listKeys: string[];
detailKeys: string[];
}
type PostManagerEvent =
| CacheEvent<PostListPayload>
| CacheEvent<PostDetailPayload>
| CacheEvent<PostManagerSnapshot>
| CacheEvent<{ error: unknown; context: string }>;
const LIST_TTL = 30 * 1000;
const DETAIL_TTL = 60 * 1000;
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
class PostManager extends CacheBus<PostManagerSnapshot, PostManagerEvent> {
private listCache = new Map<string, CacheEntry<Post[]>>();
private detailCache = new Map<string, CacheEntry<Post | null>>();
private pendingRequests = new Map<string, Promise<any>>();
protected getSnapshot(): PostManagerSnapshot {
return {
listKeys: [...this.listCache.keys()],
detailKeys: [...this.detailCache.keys()],
};
}
private isExpired(entry: CacheEntry<any>): boolean {
return Date.now() - entry.timestamp > entry.ttl;
}
private async dedupe<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const pending = this.pendingRequests.get(key) as Promise<T> | undefined;
if (pending) return pending;
const request = fetcher().finally(() => {
this.pendingRequests.delete(key);
});
this.pendingRequests.set(key, request);
return request;
}
private listKey(type: string, page: number, pageSize: number): string {
return `${type}:${page}:${pageSize}`;
}
async getPosts(
type = 'recommend',
page = 1,
pageSize = 20,
forceRefresh = false
): Promise<Post[]> {
const key = this.listKey(type, page, pageSize);
const cached = this.listCache.get(key);
if (!forceRefresh && cached && !this.isExpired(cached)) {
return cached.data;
}
if (!forceRefresh && cached && this.isExpired(cached)) {
this.refreshPostsInBackground(key, type, page, pageSize);
return cached.data;
}
return this.dedupe(`posts:list:${key}`, async () => {
const response = await postService.getPosts(page, pageSize, type);
const posts = response.list || [];
this.listCache.set(key, { data: posts, timestamp: Date.now(), ttl: LIST_TTL });
this.notify({
type: 'list_updated',
payload: { key, posts },
timestamp: Date.now(),
});
return posts;
});
}
private refreshPostsInBackground(key: string, type: string, page: number, pageSize: number): void {
this.dedupe(`posts:list:bg:${key}`, async () => {
const response = await postService.getPosts(page, pageSize, type);
const posts = response.list || [];
this.listCache.set(key, { data: posts, timestamp: Date.now(), ttl: LIST_TTL });
this.notify({
type: 'list_updated',
payload: { key, posts },
timestamp: Date.now(),
});
return posts;
}).catch((error) => {
this.notify({
type: 'error',
payload: { error, context: 'refreshPostsInBackground' },
timestamp: Date.now(),
});
});
}
async getPostDetail(postId: string, forceRefresh = false): Promise<Post | null> {
const cached = this.detailCache.get(postId);
if (!forceRefresh && cached && !this.isExpired(cached)) {
return cached.data;
}
if (!forceRefresh && cached && this.isExpired(cached)) {
this.refreshPostDetailInBackground(postId);
return cached.data;
}
return this.dedupe(`posts:detail:${postId}`, async () => {
const post = await postService.getPost(postId);
this.detailCache.set(postId, { data: post, timestamp: Date.now(), ttl: DETAIL_TTL });
this.notify({
type: 'detail_updated',
payload: { postId, post },
timestamp: Date.now(),
});
return post;
});
}
private refreshPostDetailInBackground(postId: string): void {
this.dedupe(`posts:detail:bg:${postId}`, async () => {
const post = await postService.getPost(postId);
this.detailCache.set(postId, { data: post, timestamp: Date.now(), ttl: DETAIL_TTL });
this.notify({
type: 'detail_updated',
payload: { postId, post },
timestamp: Date.now(),
});
return post;
}).catch((error) => {
this.notify({
type: 'error',
payload: { error, context: 'refreshPostDetailInBackground' },
timestamp: Date.now(),
});
});
}
invalidate(postId?: string): void {
if (postId) {
this.detailCache.delete(postId);
return;
}
this.listCache.clear();
this.detailCache.clear();
}
clear(): void {
this.listCache.clear();
this.detailCache.clear();
this.pendingRequests.clear();
}
}
export const postManager = new PostManager();

188
src/stores/userManager.ts Normal file
View File

@@ -0,0 +1,188 @@
import { UserDTO } from '../types/dto';
import { authService } from '../services/authService';
import {
getCurrentUserCache,
getUserCache,
saveCurrentUserCache,
saveUserCache,
} from '../services/database';
import { CacheBus, CacheEvent } from './cacheBus';
interface UserManagerSnapshot {
hasCurrentUser: boolean;
cachedUserIds: string[];
}
type UserManagerEvent =
| CacheEvent<{ user: UserDTO | null }>
| CacheEvent<{ userId: string; user: UserDTO | null }>
| CacheEvent<UserManagerSnapshot>
| CacheEvent<{ error: unknown; context: string }>;
const USER_TTL = 5 * 60 * 1000;
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
class UserManager extends CacheBus<UserManagerSnapshot, UserManagerEvent> {
private currentUserCache: CacheEntry<UserDTO | null> | null = null;
private userCache = new Map<string, CacheEntry<UserDTO | null>>();
private pendingRequests = new Map<string, Promise<any>>();
protected getSnapshot(): UserManagerSnapshot {
return {
hasCurrentUser: !!this.currentUserCache?.data,
cachedUserIds: [...this.userCache.keys()],
};
}
private isExpired(entry: CacheEntry<any>): boolean {
return Date.now() - entry.timestamp > entry.ttl;
}
private async dedupe<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const pending = this.pendingRequests.get(key) as Promise<T> | undefined;
if (pending) return pending;
const request = fetcher().finally(() => {
this.pendingRequests.delete(key);
});
this.pendingRequests.set(key, request);
return request;
}
async getCurrentUser(forceRefresh = false): Promise<UserDTO | null> {
if (!forceRefresh && this.currentUserCache && !this.isExpired(this.currentUserCache)) {
return this.currentUserCache.data;
}
if (!forceRefresh && this.currentUserCache && this.isExpired(this.currentUserCache)) {
this.refreshCurrentUserInBackground();
return this.currentUserCache.data;
}
if (!forceRefresh) {
const cachedFromDb = await getCurrentUserCache();
if (cachedFromDb) {
this.currentUserCache = { data: cachedFromDb, timestamp: Date.now(), ttl: USER_TTL };
this.notify({
type: 'detail_updated',
payload: { user: cachedFromDb },
timestamp: Date.now(),
});
this.refreshCurrentUserInBackground();
return cachedFromDb;
}
}
return this.dedupe('users:current', async () => {
const user = await authService.fetchCurrentUserFromAPI();
this.currentUserCache = { data: user, timestamp: Date.now(), ttl: USER_TTL };
if (user) {
saveCurrentUserCache(user).catch(() => {});
saveUserCache(user).catch(() => {});
}
this.notify({
type: 'detail_updated',
payload: { user },
timestamp: Date.now(),
});
return user;
});
}
private refreshCurrentUserInBackground(): void {
this.dedupe('users:current:bg', async () => {
const user = await authService.fetchCurrentUserFromAPI();
this.currentUserCache = { data: user, timestamp: Date.now(), ttl: USER_TTL };
if (user) {
saveCurrentUserCache(user).catch(() => {});
saveUserCache(user).catch(() => {});
}
this.notify({
type: 'detail_updated',
payload: { user },
timestamp: Date.now(),
});
return user;
}).catch((error) => {
this.notify({
type: 'error',
payload: { error, context: 'refreshCurrentUserInBackground' },
timestamp: Date.now(),
});
});
}
async getUserById(userId: string, forceRefresh = false): Promise<UserDTO | null> {
const cached = this.userCache.get(userId);
if (!forceRefresh && cached && !this.isExpired(cached)) {
return cached.data;
}
if (!forceRefresh && cached && this.isExpired(cached)) {
this.refreshUserByIdInBackground(userId);
return cached.data;
}
if (!forceRefresh) {
const cachedFromDb = await getUserCache(userId);
if (cachedFromDb) {
this.userCache.set(userId, { data: cachedFromDb, timestamp: Date.now(), ttl: USER_TTL });
this.notify({
type: 'detail_updated',
payload: { userId, user: cachedFromDb },
timestamp: Date.now(),
});
this.refreshUserByIdInBackground(userId);
return cachedFromDb;
}
}
return this.dedupe(`users:detail:${userId}`, async () => {
const user = await authService.fetchUserByIdFromAPI(userId);
this.userCache.set(userId, { data: user, timestamp: Date.now(), ttl: USER_TTL });
if (user) {
saveUserCache(user).catch(() => {});
}
this.notify({
type: 'detail_updated',
payload: { userId, user },
timestamp: Date.now(),
});
return user;
});
}
private refreshUserByIdInBackground(userId: string): void {
this.dedupe(`users:detail:bg:${userId}`, async () => {
const user = await authService.fetchUserByIdFromAPI(userId);
this.userCache.set(userId, { data: user, timestamp: Date.now(), ttl: USER_TTL });
if (user) {
saveUserCache(user).catch(() => {});
}
this.notify({
type: 'detail_updated',
payload: { userId, user },
timestamp: Date.now(),
});
return user;
}).catch((error) => {
this.notify({
type: 'error',
payload: { error, context: 'refreshUserByIdInBackground' },
timestamp: Date.now(),
});
});
}
invalidateUsers(): void {
this.currentUserCache = null;
this.userCache.clear();
}
}
export const userManager = new UserManager();

603
src/stores/userStore.ts Normal file
View File

@@ -0,0 +1,603 @@
/**
* 用户状态管理
* 使用Zustand进行状态管理
* 对接真实后端API
*/
import { create } from 'zustand';
import { User, Post, Notification, NotificationBadge } from '../types';
import { PaginatedData } from '../services/api';
import {
authService,
postService,
commentService,
notificationService,
} from '../services';
import { userManager } from './userManager';
import { messageManager } from './messageManager';
interface UserState {
// 状态
users: User[];
userCache: Record<string, User>; // 用户缓存
posts: Post[];
notifications: Notification[];
notificationBadge: NotificationBadge;
messageUnreadCount: number;
// 加载状态
isLoadingPosts: boolean;
isLoadingNotifications: boolean;
// 搜索历史
searchHistory: string[];
// 操作
fetchUser: (userId: string) => Promise<User | undefined>;
fetchUserPosts: (userId: string, page?: number) => Promise<Post[]>;
fetchPosts: (type: 'recommend' | 'follow' | 'hot' | 'latest', page?: number) => Promise<PaginatedData<Post>>;
fetchNotifications: (type?: string, page?: number) => Promise<Notification[]>;
markNotificationAsRead: (notificationId: string) => Promise<void>;
markAllNotificationsAsRead: () => Promise<void>;
fetchNotificationBadge: () => Promise<void>;
setMessageUnreadCount: (count: number) => void;
fetchMessageUnreadCount: () => Promise<void>;
addSearchHistory: (keyword: string) => void;
clearSearchHistory: () => void;
// 互动操作
likePost: (postId: string) => Promise<void>;
unlikePost: (postId: string) => Promise<void>;
favoritePost: (postId: string) => Promise<void>;
unfavoritePost: (postId: string) => Promise<void>;
likeComment: (commentId: string) => Promise<void>;
unlikeComment: (commentId: string) => Promise<void>;
followUser: (userId: string) => Promise<void>;
unfollowUser: (userId: string) => Promise<void>;
// 刷新数据
refreshAll: () => Promise<void>;
}
export const useUserStore = create<UserState>((set, get) => ({
// 初始状态
users: [],
userCache: {},
posts: [],
notifications: [],
notificationBadge: {
total: 0,
likes: 0,
comments: 0,
follows: 0,
system: 0,
},
messageUnreadCount: 0,
searchHistory: [],
isLoadingPosts: false,
isLoadingNotifications: false,
// 获取用户信息
fetchUser: async (userId: string) => {
// 先检查缓存
const cached = get().userCache[userId];
if (cached) return cached;
try {
const user = await userManager.getUserById(userId);
if (user) {
set(state => ({
userCache: { ...state.userCache, [userId]: user }
}));
return user;
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
return undefined;
},
// 获取用户帖子
fetchUserPosts: async (userId: string, page = 1) => {
try {
const response = await postService.getUserPosts(userId, page);
const newPosts = response.list;
set(state => ({
posts: page === 1 ? newPosts : [...state.posts, ...newPosts]
}));
return newPosts;
} catch (error) {
console.error('获取用户帖子失败:', error);
return [];
}
},
// 获取帖子列表(首页)
fetchPosts: async (type: 'recommend' | 'follow' | 'hot' | 'latest', page = 1) => {
set({ isLoadingPosts: true });
try {
let response;
switch (type) {
case 'recommend':
response = await postService.getRecommendedPosts(page);
break;
case 'follow':
response = await postService.getFollowingPosts(page);
break;
case 'hot':
response = await postService.getHotPosts(page);
break;
case 'latest':
default:
response = await postService.getLatestPosts(page);
break;
}
const newPosts = response.list;
set(state => ({
posts: page === 1 ? newPosts : [...state.posts, ...newPosts],
isLoadingPosts: false
}));
return response;
} catch (error) {
console.error('获取帖子列表失败:', error);
set({ isLoadingPosts: false });
return {
list: [],
total: 0,
page,
page_size: 20,
total_pages: 0,
};
}
},
// 获取通知列表
fetchNotifications: async (type?: string, page = 1) => {
set({ isLoadingNotifications: true });
try {
const response = await notificationService.getNotifications(
page,
20,
type as any
);
const newNotifications = response.list;
set(state => ({
notifications: page === 1 ? newNotifications : [...state.notifications, ...newNotifications],
isLoadingNotifications: false
}));
return newNotifications;
} catch (error) {
console.error('获取通知列表失败:', error);
set({ isLoadingNotifications: false });
return [];
}
},
// 标记通知为已读
markNotificationAsRead: async (notificationId: string) => {
try {
await notificationService.markAsRead(notificationId);
set(state => {
const notifications = state.notifications.map(n =>
n.id === notificationId ? { ...n, isRead: true } : n
);
return {
notifications,
notificationBadge: {
total: notifications.filter(n => !n.isRead).length,
likes: notifications.filter(n => n.type === 'like_post' && !n.isRead).length,
comments: notifications.filter(n => n.type === 'comment' && !n.isRead).length,
follows: notifications.filter(n => n.type === 'follow' && !n.isRead).length,
system: notifications.filter(n => n.type === 'system' && !n.isRead).length,
}
};
});
} catch (error) {
console.error('标记通知为已读失败:', error);
}
},
// 标记所有通知为已读
markAllNotificationsAsRead: async () => {
try {
await notificationService.markAllAsRead();
set(state => ({
notifications: state.notifications.map(n => ({ ...n, isRead: true })),
notificationBadge: {
total: 0,
likes: 0,
comments: 0,
follows: 0,
system: 0,
}
}));
} catch (error) {
console.error('标记所有通知为已读失败:', error);
}
},
// 获取通知角标
fetchNotificationBadge: async () => {
try {
const badge = await notificationService.getNotificationBadge();
set({ notificationBadge: badge });
} catch (error) {
console.error('获取通知角标失败:', error);
}
},
// 设置消息未读数
// 注意:未读数现在由 MessageManager 统一管理
// 此方法保留用于 TabBar 角标显示,从 MessageManager 同步
setMessageUnreadCount: (count: number) => {
set({ messageUnreadCount: count });
},
// 从后端拉取最新消息未读数
// @deprecated 请使用 messageManager.fetchUnreadCount() 代替
// 此方法保留用于向后兼容
fetchMessageUnreadCount: async () => {
try {
await messageManager.fetchUnreadCount();
const unread = messageManager.getUnreadCount();
set({ messageUnreadCount: unread.total + unread.system });
} catch (error) {
console.error('获取消息未读数失败:', error);
}
},
// 添加搜索历史
addSearchHistory: (keyword: string) => {
set(state => {
const history = [keyword, ...state.searchHistory.filter(k => k !== keyword)].slice(0, 20);
return { searchHistory: history };
});
},
// 清除搜索历史
clearSearchHistory: () => {
set({ searchHistory: [] });
},
// 点赞帖子 - 乐观更新
likePost: async (postId: string) => {
console.log('[userStore] likePost called:', postId);
// 先乐观更新本地状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_liked: true, likes_count: p.likes_count + 1 } : p
)
}));
// 调用API
try {
const updatedPost = await postService.likePost(postId);
if (updatedPost) {
console.log('[userStore] likePost success, updated post:', updatedPost);
// 使用后端返回的更新后数据更新状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? updatedPost : p
)
}));
} else {
// API 返回失败,回滚状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_liked: false, likes_count: Math.max(0, p.likes_count - 1) } : p
)
}));
}
} catch (error) {
console.error('点赞帖子失败:', error);
// 失败回滚状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_liked: false, likes_count: Math.max(0, p.likes_count - 1) } : p
)
}));
}
},
// 取消点赞 - 乐观更新
unlikePost: async (postId: string) => {
console.log('[userStore] unlikePost called:', postId);
// 先乐观更新本地状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_liked: false, likes_count: Math.max(0, p.likes_count - 1) } : p
)
}));
// 调用API
try {
const updatedPost = await postService.unlikePost(postId);
if (updatedPost) {
console.log('[userStore] unlikePost success, updated post:', updatedPost);
// 使用后端返回的更新后数据更新状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? updatedPost : p
)
}));
} else {
// API 返回失败,回滚状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_liked: true, likes_count: p.likes_count + 1 } : p
)
}));
}
} catch (error) {
console.error('取消点赞帖子失败:', error);
// 失败回滚状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_liked: true, likes_count: p.likes_count + 1 } : p
)
}));
}
},
// 收藏帖子 - 乐观更新
favoritePost: async (postId: string) => {
console.log('[userStore] favoritePost called:', postId);
// 先乐观更新本地状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_favorited: true, favorites_count: p.favorites_count + 1 } : p
)
}));
// 调用API
try {
const updatedPost = await postService.favoritePost(postId);
if (updatedPost) {
console.log('[userStore] favoritePost success, updated post:', updatedPost);
// 使用后端返回的更新后数据更新状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? updatedPost : p
)
}));
} else {
// API 返回失败,回滚状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_favorited: false, favorites_count: Math.max(0, p.favorites_count - 1) } : p
)
}));
}
} catch (error) {
console.error('收藏帖子失败:', error);
// 失败回滚状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_favorited: false, favorites_count: Math.max(0, p.favorites_count - 1) } : p
)
}));
}
},
// 取消收藏 - 乐观更新
unfavoritePost: async (postId: string) => {
console.log('[userStore] unfavoritePost called:', postId);
// 先乐观更新本地状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_favorited: false, favorites_count: Math.max(0, p.favorites_count - 1) } : p
)
}));
// 调用API
try {
const updatedPost = await postService.unfavoritePost(postId);
if (updatedPost) {
console.log('[userStore] unfavoritePost success, updated post:', updatedPost);
// 使用后端返回的更新后数据更新状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? updatedPost : p
)
}));
} else {
// API 返回失败,回滚状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_favorited: true, favorites_count: p.favorites_count + 1 } : p
)
}));
}
} catch (error) {
console.error('取消收藏帖子失败:', error);
// 失败回滚状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_favorited: true, favorites_count: p.favorites_count + 1 } : p
)
}));
}
},
// 点赞评论
likeComment: async (commentId: string) => {
console.log('[userStore] likeComment called:', commentId);
// 先更新本地状态更新posts中的帖子的评论点赞状态
set(state => ({
posts: state.posts.map(p => ({
...p,
top_comment: p.top_comment && p.top_comment.id === commentId
? { ...p.top_comment, is_liked: true, likes_count: p.top_comment.likes_count + 1 }
: p.top_comment
}))
}));
// 调用API
try {
await commentService.likeComment(commentId);
console.log('[userStore] likeComment success:', commentId);
} catch (error) {
console.error('点赞评论失败:', error);
// 失败回滚状态
set(state => ({
posts: state.posts.map(p => ({
...p,
top_comment: p.top_comment && p.top_comment.id === commentId
? { ...p.top_comment, is_liked: false, likes_count: Math.max(0, p.top_comment.likes_count - 1) }
: p.top_comment
}))
}));
}
},
// 取消点赞评论
unlikeComment: async (commentId: string) => {
console.log('[userStore] unlikeComment called:', commentId);
// 先更新本地状态
set(state => ({
posts: state.posts.map(p => ({
...p,
top_comment: p.top_comment && p.top_comment.id === commentId
? { ...p.top_comment, is_liked: false, likes_count: Math.max(0, p.top_comment.likes_count - 1) }
: p.top_comment
}))
}));
// 调用API
try {
await commentService.unlikeComment(commentId);
console.log('[userStore] unlikeComment success:', commentId);
} catch (error) {
console.error('取消点赞评论失败:', error);
// 失败回滚状态
set(state => ({
posts: state.posts.map(p => ({
...p,
top_comment: p.top_comment && p.top_comment.id === commentId
? { ...p.top_comment, is_liked: true, likes_count: p.top_comment.likes_count + 1 }
: p.top_comment
}))
}));
}
},
// 关注用户
followUser: async (userId: string) => {
// 先更新本地状态
set(state => ({
users: state.users.map(u =>
u.id === userId
? { ...u, isFollowing: true, followersCount: u.followers_count + 1 }
: u
),
userCache: Object.fromEntries(
Object.entries(state.userCache).map(([id, user]) => [
id,
id === userId
? { ...user, isFollowing: true, followersCount: user.followers_count + 1 }
: user
])
)
}));
// 调用API
try {
await authService.followUser(userId);
} catch (error) {
console.error('关注用户失败:', error);
// 失败回滚状态
set(state => ({
users: state.users.map(u =>
u.id === userId
? { ...u, isFollowing: false, followersCount: Math.max(0, u.followers_count - 1) }
: u
),
userCache: Object.fromEntries(
Object.entries(state.userCache).map(([id, user]) => [
id,
id === userId
? { ...user, isFollowing: false, followersCount: Math.max(0, user.followers_count - 1) }
: user
])
)
}));
}
},
// 取消关注
unfollowUser: async (userId: string) => {
// 先更新本地状态
set(state => ({
users: state.users.map(u =>
u.id === userId
? { ...u, isFollowing: false, followersCount: Math.max(0, u.followers_count - 1) }
: u
),
userCache: Object.fromEntries(
Object.entries(state.userCache).map(([id, user]) => [
id,
id === userId
? { ...user, isFollowing: false, followersCount: Math.max(0, user.followers_count - 1) }
: user
])
)
}));
// 调用API
try {
await authService.unfollowUser(userId);
} catch (error) {
console.error('取消关注用户失败:', error);
// 失败回滚状态
set(state => ({
users: state.users.map(u =>
u.id === userId
? { ...u, isFollowing: true, followersCount: u.followers_count + 1 }
: u
),
userCache: Object.fromEntries(
Object.entries(state.userCache).map(([id, user]) => [
id,
id === userId
? { ...user, isFollowing: true, followersCount: user.followers_count + 1 }
: user
])
)
}));
}
},
// 刷新所有数据
refreshAll: async () => {
await Promise.all([
get().fetchPosts('recommend', 1),
get().fetchNotificationBadge(),
]);
},
}));
// 导出selector hooks以优化性能
export const usePosts = () => useUserStore((state) => state.posts);
export const useNotifications = () => useUserStore((state) => state.notifications);
export const useNotificationBadge = () => useUserStore((state) => state.notificationBadge);
export const useMessageUnreadCount = () => useUserStore((state) => state.messageUnreadCount);
export const useSearchHistory = () => useUserStore((state) => state.searchHistory);
export const useIsLoadingPosts = () => useUserStore((state) => state.isLoadingPosts);