- 优化提醒分组逻辑,精确判断过期时间 - 已完成但未过期的提醒仍显示在主列表(划掉状态) - 修复 checkbox 点击事件处理 - 添加乐观更新,UI 即时响应 - 添加归档页和设置页路由 - 修复后端 is_completed 字段验证问题
241 lines
7.5 KiB
TypeScript
241 lines
7.5 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
import type { User, Event, Note, AIConversation, EventType } from '../types';
|
|
import { api } from '../services/api';
|
|
|
|
// 应用设置类型
|
|
interface AppSettings {
|
|
showHolidays: boolean; // 是否显示节假日
|
|
}
|
|
|
|
// 默认设置
|
|
const defaultSettings: AppSettings = {
|
|
showHolidays: true,
|
|
};
|
|
|
|
interface AppState {
|
|
// Auth state
|
|
user: User | null;
|
|
isAuthenticated: boolean;
|
|
isLoading: boolean;
|
|
|
|
// Settings state
|
|
settings: AppSettings;
|
|
updateSettings: (settings: Partial<AppSettings>) => void;
|
|
|
|
// Data state
|
|
events: Event[];
|
|
notes: Note | null;
|
|
conversations: AIConversation[];
|
|
|
|
// Actions
|
|
setUser: (user: User | null) => void;
|
|
setLoading: (loading: boolean) => void;
|
|
setEvents: (events: Event[]) => void;
|
|
addEvent: (event: Event) => void;
|
|
updateEvent: (id: string, updates: Partial<Event>) => void;
|
|
deleteEvent: (id: string) => void;
|
|
setNotes: (notes: Note | null) => void;
|
|
updateNotesContent: (content: string) => void;
|
|
setConversations: (conversations: AIConversation[]) => void;
|
|
addConversation: (conversation: AIConversation) => void;
|
|
|
|
// Data fetch actions
|
|
fetchEvents: (type?: EventType) => Promise<void>;
|
|
fetchNotes: () => Promise<void>;
|
|
saveNotes: (content: string) => Promise<void>;
|
|
createEvent: (event: Partial<Event>) => Promise<{ error: any }>;
|
|
updateEventById: (id: string, event: Partial<Event>) => Promise<{ error: any }>;
|
|
deleteEventById: (id: string) => Promise<{ error: any }>;
|
|
|
|
// Auth actions
|
|
login: (email: string, password: string) => Promise<{ error: any }>;
|
|
register: (email: string, password: string, nickname?: string) => Promise<{ error: any }>;
|
|
logout: () => Promise<void>;
|
|
checkAuth: () => Promise<void>;
|
|
}
|
|
|
|
export const useAppStore = create<AppState>()(
|
|
persist(
|
|
(set) => ({
|
|
// Initial state
|
|
user: null,
|
|
isAuthenticated: false,
|
|
isLoading: true,
|
|
settings: defaultSettings,
|
|
events: [],
|
|
notes: null,
|
|
conversations: [],
|
|
|
|
// Settings
|
|
updateSettings: (newSettings) =>
|
|
set((state) => ({
|
|
settings: { ...state.settings, ...newSettings },
|
|
})),
|
|
|
|
// Setters
|
|
setUser: (user) => set({ user, isAuthenticated: !!user }),
|
|
setLoading: (isLoading) => set({ isLoading }),
|
|
setEvents: (events) => set({ events }),
|
|
addEvent: (event) => set((state) => ({ events: [...state.events, event] })),
|
|
updateEvent: (id, updates) => set((state) => ({
|
|
events: state.events.map((e) => (e.id === id ? { ...e, ...updates } : e)),
|
|
})),
|
|
deleteEvent: (id) => set((state) => ({
|
|
events: state.events.filter((e) => e.id !== id),
|
|
})),
|
|
setNotes: (notes) => set({ notes }),
|
|
updateNotesContent: (content) => set((state) => ({
|
|
notes: state.notes ? { ...state.notes, content } : null,
|
|
})),
|
|
setConversations: (conversations) => set({ conversations }),
|
|
addConversation: (conversation) => set((state) => ({
|
|
conversations: [conversation, ...state.conversations],
|
|
})),
|
|
|
|
// Data fetch actions
|
|
fetchEvents: async (type) => {
|
|
try {
|
|
const events = await api.events.list(type);
|
|
set({ events });
|
|
} catch (error) {
|
|
console.error('Failed to fetch events:', error);
|
|
}
|
|
},
|
|
|
|
fetchNotes: async () => {
|
|
try {
|
|
const notes = await api.notes.get();
|
|
set({ notes });
|
|
} catch (error) {
|
|
console.error('Failed to fetch notes:', error);
|
|
}
|
|
},
|
|
|
|
saveNotes: async (content) => {
|
|
try {
|
|
const notes = await api.notes.update(content);
|
|
set({ notes });
|
|
} catch (error) {
|
|
console.error('Failed to save notes:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
createEvent: async (event) => {
|
|
try {
|
|
const newEvent = await api.events.create(event);
|
|
set((state) => ({ events: [...state.events, newEvent] }));
|
|
return { error: null };
|
|
} catch (error: any) {
|
|
return { error: error.message || '创建失败' };
|
|
}
|
|
},
|
|
|
|
updateEventById: async (id, event) => {
|
|
try {
|
|
// 乐观更新:立即更新本地状态
|
|
set((state) => ({
|
|
events: state.events.map((e) =>
|
|
e.id === id ? { ...e, ...event } : e
|
|
),
|
|
}));
|
|
// 发送 API 请求
|
|
await api.events.update(id, event);
|
|
// 乐观更新已生效,不需要用 API 返回数据覆盖
|
|
// 避免 API 返回数据不完整导致状态丢失
|
|
return { error: null };
|
|
} catch (error: any) {
|
|
// 失败时回滚,重新获取数据
|
|
const events = await api.events.list();
|
|
set({ events });
|
|
return { error: error.message || '更新失败' };
|
|
}
|
|
},
|
|
|
|
deleteEventById: async (id) => {
|
|
try {
|
|
await api.events.delete(id);
|
|
set((state) => ({
|
|
events: state.events.filter((e) => e.id !== id),
|
|
}));
|
|
return { error: null };
|
|
} catch (error: any) {
|
|
return { error: error.message || '删除失败' };
|
|
}
|
|
},
|
|
|
|
// Auth actions
|
|
login: async (email, password) => {
|
|
try {
|
|
const { user, token, refreshToken } = await api.auth.login(email, password);
|
|
api.setTokens(token, refreshToken);
|
|
set({ user, isAuthenticated: true });
|
|
return { error: null };
|
|
} catch (error: any) {
|
|
const errorMessage = error.message || '登录失败,请检查邮箱和密码';
|
|
return { error: errorMessage };
|
|
}
|
|
},
|
|
|
|
register: async (email, password, nickname) => {
|
|
try {
|
|
const { user, token, refreshToken } = await api.auth.register(email, password, nickname);
|
|
api.setTokens(token, refreshToken);
|
|
set({ user, isAuthenticated: true });
|
|
return { error: null };
|
|
} catch (error: any) {
|
|
const errorMessage = error.message || '注册失败,请稍后重试';
|
|
return { error: errorMessage };
|
|
}
|
|
},
|
|
|
|
logout: async () => {
|
|
try {
|
|
await api.auth.logout();
|
|
} catch {
|
|
// Ignore logout error
|
|
}
|
|
api.clearTokens();
|
|
set({ user: null, isAuthenticated: false, events: [], notes: null });
|
|
},
|
|
|
|
checkAuth: async () => {
|
|
set({ isLoading: true });
|
|
const token = api.getToken();
|
|
|
|
if (!token) {
|
|
set({ isAuthenticated: false, isLoading: false });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { user } = await api.auth.getCurrentUser();
|
|
set({ user, isAuthenticated: true, isLoading: false });
|
|
} catch {
|
|
// Token invalid, try refresh
|
|
try {
|
|
const { token: newToken } = await api.auth.refreshToken();
|
|
const { user } = await api.auth.getCurrentUser();
|
|
const refreshToken = api.getRefreshToken()!;
|
|
api.setTokens(newToken, refreshToken);
|
|
set({ user, isAuthenticated: true, isLoading: false });
|
|
} catch {
|
|
api.clearTokens();
|
|
set({ isAuthenticated: false, isLoading: false });
|
|
}
|
|
}
|
|
},
|
|
}),
|
|
{
|
|
name: 'qia-storage',
|
|
storage: createJSONStorage(() => localStorage),
|
|
partialize: (state) => ({
|
|
user: state.user,
|
|
isAuthenticated: state.isAuthenticated,
|
|
settings: state.settings,
|
|
}),
|
|
}
|
|
)
|
|
);
|