主要修复: 1. 时间存储格式统一使用 UTC 时区 - HomePage.tsx: 修复 formDate 处理,使用 new Date() 自动转换 - 解决用户设置"14:00"保存后显示"22:00"问题 2. 重复计算函数统一使用 UTC - calculateNextDueDate: 使用 getUTC*() 和 Date.UTC() - calculateReminderTimes: 使用 UTC 时间计算提醒点 - getReminderValueFromTimes: 使用 UTC 时间戳反推选项 3. 修复重复提醒创建时继承 reminder_times - createNextRecurringEventData: 根据新日期重新计算 reminder_times - 修改接口类型支持 reminder_times 为 null - 解决原提醒"提前15分钟"新提醒变"准时"问题 影响: - 提醒时间显示正确(无时区偏差) - 跨天/跨月提醒计算正确 - 重复提醒自动创建时正确继承提醒设置 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
308 lines
10 KiB
TypeScript
308 lines
10 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 { calculateNextReminderDate, findNextValidReminderDate, isDuplicateReminder, createNextRecurringEventData } from '../utils/repeatCalculator';
|
||
|
||
// 应用设置类型
|
||
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 的回调函数获取当前状态
|
||
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);
|
||
}
|
||
|
||
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,
|
||
}),
|
||
}
|
||
)
|
||
);
|