- 扩展设置选项:显示数量选择(1/3/5/10个) - 添加仅显示法定节假日开关 - 添加节假日筛选功能:可选择关注特定节假日 - 更新 AnniversaryList 使用新设置进行过滤 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
328 lines
11 KiB
TypeScript
328 lines
11 KiB
TypeScript
import { create } from 'zustand';
|
||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||
import type { User, Event, Note, AIConversation, EventType, RepeatType } from '../types';
|
||
import { api } from '../services/api';
|
||
import { syncRemindersToSW } from '../services/swSync';
|
||
import { calculateNextReminderDate, findNextValidReminderDate, isDuplicateReminder, createNextRecurringEventData } from '../utils/repeatCalculator';
|
||
|
||
// 应用设置类型
|
||
interface AppSettings {
|
||
showHolidays: boolean; // 是否显示节假日
|
||
holidayDisplayCount: number; // 节假日显示数量
|
||
showStatutoryOnly: boolean; // 仅显示法定节假日
|
||
enabledHolidays: string[]; // 用户关注的节假日ID列表(空表示全部)
|
||
browserNotifications: boolean; // 是否启用浏览器通知
|
||
}
|
||
|
||
// 默认设置
|
||
const defaultSettings: AppSettings = {
|
||
showHolidays: true,
|
||
holidayDisplayCount: 3,
|
||
showStatutoryOnly: false,
|
||
enabledHolidays: [],
|
||
browserNotifications: false,
|
||
};
|
||
|
||
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] }));
|
||
|
||
// 如果是提醒事件,同步到 Service Worker
|
||
if (event.type === 'reminder' && event.reminder_times && event.reminder_times.length > 0) {
|
||
await syncRemindersToSW();
|
||
}
|
||
|
||
return { error: null };
|
||
} catch (error: any) {
|
||
return { error: error.message || '创建失败' };
|
||
}
|
||
},
|
||
|
||
updateEventById: async (id, event) => {
|
||
try {
|
||
// 使用 set 的回调函数获取当前状态
|
||
let currentEvent: Event | undefined;
|
||
let allEvents: Event[] = [];
|
||
set((state) => {
|
||
allEvents = state.events;
|
||
currentEvent = state.events.find((e) => e.id === id);
|
||
return {
|
||
events: state.events.map((e) =>
|
||
e.id === id ? { ...e, ...event } : e
|
||
),
|
||
};
|
||
});
|
||
|
||
if (!currentEvent) {
|
||
return { error: '事件不存在' };
|
||
}
|
||
|
||
// 如果是标记完成,且是重复提醒,自动创建下一周期,并移除当前提醒的重复设置
|
||
if (event.is_completed && currentEvent.repeat_type !== 'none') {
|
||
// 从 next_reminder_date 开始查找正确的下一个提醒日期
|
||
const nextReminderDate = currentEvent.next_reminder_date || currentEvent.date;
|
||
const nextValidDate = findNextValidReminderDate(
|
||
nextReminderDate,
|
||
currentEvent.repeat_type as RepeatType,
|
||
currentEvent.repeat_interval
|
||
);
|
||
|
||
// 检查是否已存在相同的提醒(去重)
|
||
const exists = isDuplicateReminder(
|
||
allEvents,
|
||
currentEvent.title,
|
||
nextValidDate,
|
||
currentEvent.repeat_type as RepeatType
|
||
);
|
||
|
||
if (!exists) {
|
||
// 使用统一的重复事件创建函数,自动继承所有字段(包括 priority 和 reminder_times)
|
||
const newEventData = createNextRecurringEventData(
|
||
currentEvent,
|
||
nextValidDate
|
||
);
|
||
const newEvent = await api.events.create(newEventData);
|
||
|
||
// 添加新事件到本地状态
|
||
set((state) => ({
|
||
events: [...state.events, newEvent],
|
||
}));
|
||
}
|
||
|
||
// 发送 API 请求:标记为已完成并移除重复设置(合并为一个请求)
|
||
await api.events.update(id, {
|
||
is_completed: true,
|
||
repeat_type: 'none',
|
||
repeat_interval: null,
|
||
next_reminder_date: null,
|
||
});
|
||
|
||
// 更新本地状态:标记为已完成并移除重复设置
|
||
set((state) => ({
|
||
events: state.events.map((e) =>
|
||
e.id === id
|
||
? {
|
||
...e,
|
||
is_completed: true,
|
||
repeat_type: 'none',
|
||
repeat_interval: null,
|
||
next_reminder_date: null,
|
||
}
|
||
: e
|
||
),
|
||
}));
|
||
} else {
|
||
// 非完成操作,正常发送 API 请求
|
||
await api.events.update(id, event);
|
||
}
|
||
|
||
// 如果更新涉及提醒设置,同步到 Service Worker
|
||
if (event.reminder_times || event.type === 'reminder') {
|
||
await syncRemindersToSW();
|
||
}
|
||
|
||
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,
|
||
}),
|
||
}
|
||
)
|
||
);
|