qia-client/src/stores/index.ts
ddshi b39bc5c8bc fix: 修复提醒时间相关的 UTC 时区和继承问题
主要修复:
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>
2026-02-10 11:07:38 +08:00

308 lines
10 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 { 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,
}),
}
)
);