qia-client/src/stores/index.ts
ddshi e7b6864b42 feat: 设置中添加节假日配置功能
- 扩展设置选项:显示数量选择(1/3/5/10个)
- 添加仅显示法定节假日开关
- 添加节假日筛选功能:可选择关注特定节假日
- 更新 AnniversaryList 使用新设置进行过滤

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:23:49 +08:00

328 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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