diff --git a/src/components/anniversary/AnniversaryList.tsx b/src/components/anniversary/AnniversaryList.tsx index ec8c6af..6b59323 100644 --- a/src/components/anniversary/AnniversaryList.tsx +++ b/src/components/anniversary/AnniversaryList.tsx @@ -4,6 +4,7 @@ import { IconPlus } from '@tabler/icons-react'; import { AnniversaryCard } from './AnniversaryCard'; import { getHolidaysForYear } from '../../constants/holidays'; import { calculateCountdown, formatCountdown } from '../../utils/countdown'; +import { useAppStore } from '../../stores'; import type { Event } from '../../types'; interface AnniversaryListProps { @@ -26,9 +27,12 @@ interface BuiltInHolidayEvent { export function AnniversaryList({ events, onEventClick, onAddClick }: AnniversaryListProps) { const anniversaries = events.filter((e) => e.type === 'anniversary'); + const showHolidays = useAppStore((state) => state.settings?.showHolidays ?? true); // 获取内置节假日 const builtInHolidays = useMemo(() => { + if (!showHolidays) return []; + const now = new Date(); const year = now.getFullYear(); const holidays = getHolidaysForYear(year); @@ -39,13 +43,13 @@ export function AnniversaryList({ events, onEventClick, onAddClick }: Anniversar (a, b) => a.date.getTime() - b.date.getTime() ); - // 只取未来30天内的节假日 + // 只取未来90天内的节假日,显示最近3个 const cutoffDate = new Date(now); - cutoffDate.setDate(cutoffDate.getDate() + 30); + cutoffDate.setDate(cutoffDate.getDate() + 90); return allHolidays .filter((h) => h.date >= now && h.date <= cutoffDate) - .slice(0, 5) + .slice(0, 3) .map((h): BuiltInHolidayEvent => ({ id: `builtin-${h.id}`, title: h.name, @@ -56,7 +60,7 @@ export function AnniversaryList({ events, onEventClick, onAddClick }: Anniversar type: 'anniversary', is_builtin: true, })); - }, []); + }, [showHolidays]); // 合并用户纪念日和内置节假日 const allAnniversaries = useMemo(() => { diff --git a/src/components/reminder/ReminderCard.tsx b/src/components/reminder/ReminderCard.tsx index 05075fa..cccd632 100644 --- a/src/components/reminder/ReminderCard.tsx +++ b/src/components/reminder/ReminderCard.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; import { Paper, Text, Checkbox, Group, Stack, ActionIcon } from '@mantine/core'; -import { IconCheck, IconDots } from '@tabler/icons-react'; +import { IconDots } from '@tabler/icons-react'; import type { Event } from '../../types'; interface ReminderCardProps { @@ -45,7 +45,6 @@ export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) { setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} style={{ @@ -67,12 +66,11 @@ export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) { e.stopPropagation(); onToggle(); }} - onClick={(e) => e.stopPropagation()} size="xs" color="#1a1a1a" /> - + {/* Title */} - {isHovered && !isCompleted && ( - { - e.stopPropagation(); - onToggle(); - }} - style={{ color: '#666' }} - > - - - )} - e.stopPropagation()} + onClick={(e) => { + e.stopPropagation(); + onClick(); + }} style={{ color: '#999' }} + title="编辑" > diff --git a/src/components/reminder/ReminderList.tsx b/src/components/reminder/ReminderList.tsx index fcb69f3..8807ac7 100644 --- a/src/components/reminder/ReminderList.tsx +++ b/src/components/reminder/ReminderList.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { Stack, Text, @@ -6,16 +6,12 @@ import { Group, Button, Alert, - ActionIcon, - Tooltip, } from '@mantine/core'; import { IconPlus, IconAlertCircle, - IconArchive, } from '@tabler/icons-react'; import { ReminderCard } from './ReminderCard'; -import { ArchiveReminderModal } from './ArchiveReminderModal'; import type { Event } from '../../types'; interface ReminderListProps { @@ -35,44 +31,47 @@ export function ReminderList({ onDelete, onRestore, }: ReminderListProps) { - const [archiveOpened, setArchiveOpened] = useState(false); const grouped = useMemo(() => { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + today.setHours(0, 0, 0, 0); const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1); - const nextWeek = new Date(today); - nextWeek.setDate(nextWeek.getDate() + 7); + tomorrow.setHours(0, 0, 0, 0); + const dayAfterTomorrow = new Date(tomorrow); + dayAfterTomorrow.setDate(dayAfterTomorrow.getDate() + 1); + dayAfterTomorrow.setHours(0, 0, 0, 0); const reminders = events.filter((e) => e.type === 'reminder'); const result = { today: [] as Event[], tomorrow: [] as Event[], - thisWeek: [] as Event[], later: [] as Event[], missed: [] as Event[], - completed: [] as Event[], }; reminders.forEach((event) => { const eventDate = new Date(event.date); - // 已完成的放最后 - if (event.is_completed) { - result.completed.push(event); + // 已过期且已完成的去归档页 + if (event.is_completed && eventDate < now) { return; } - // 未完成的按时间分组 - if (eventDate < today) { + // 未过期或已完成未过期的,按时间分组 + if (eventDate < now) { + // 已过期未完成 result.missed.push(event); } else if (eventDate < tomorrow) { + // 今天 result.today.push(event); - } else if (eventDate < nextWeek) { - result.thisWeek.push(event); + } else if (eventDate < dayAfterTomorrow) { + // 明天 + result.tomorrow.push(event); } else { + // 更久之后 result.later.push(event); } }); @@ -83,10 +82,9 @@ export function ReminderList({ result.today.sort(sortByDate); result.tomorrow.sort(sortByDate); - result.thisWeek.sort(sortByDate); result.later.sort(sortByDate); - result.missed.sort(sortByDate); - result.completed.sort(sortByDate); + // 已过期按时间倒序(最近的在上面) + result.missed.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); return result; }, [events]); @@ -94,7 +92,6 @@ export function ReminderList({ const hasActiveReminders = grouped.today.length > 0 || grouped.tomorrow.length > 0 || - grouped.thisWeek.length > 0 || grouped.later.length > 0 || grouped.missed.length > 0; @@ -146,33 +143,18 @@ export function ReminderList({ )} - - {grouped.completed.length > 0 && ( - - setArchiveOpened(true)} - style={{ color: '#999' }} - > - - - - )} - - + {/* Content */} @@ -245,32 +227,12 @@ export function ReminderList({ )} - {/* 本周 */} - {grouped.thisWeek.length > 0 && ( - <> - - - 本周 - - - {grouped.thisWeek.map((event) => ( - onEventClick(event)} - onToggle={() => onToggleComplete(event)} - onDelete={onDelete ? () => onDelete(event) : undefined} - /> - ))} - - )} - - {/* 更久 */} + {/* 更久之后 */} {grouped.later.length > 0 && ( <> - 以后 + 更久之后 {grouped.later.map((event) => ( @@ -286,15 +248,6 @@ export function ReminderList({ )} - - {/* Archive Modal */} - setArchiveOpened(false)} - completedReminders={grouped.completed} - onRestore={onRestore || (() => {})} - onDelete={onDelete || (() => {})} - /> ); } diff --git a/src/constants/holidays.ts b/src/constants/holidays.ts index be3e29b..0cd8b62 100644 --- a/src/constants/holidays.ts +++ b/src/constants/holidays.ts @@ -199,7 +199,8 @@ export function getHolidaysForYear(year: number): Array state.events); + const updateEventById = useAppStore((state) => state.updateEventById); + const deleteEventById = useAppStore((state) => state.deleteEventById); + + // 页面加载时检查登录状态 + useEffect(() => { + const isAuthenticated = useAppStore.getState().isAuthenticated; + if (!isAuthenticated) { + navigate('/login', { replace: true }); + } + }, [navigate]); + + // 获取已归档的提醒(已过期且已勾选的) + const archivedReminders = events.filter( + (e) => e.type === 'reminder' && e.is_completed + ).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + + const handleRestore = async (event: Event) => { + await updateEventById(event.id, { is_completed: false }); + }; + + const handleDelete = async (event: Event) => { + await deleteEventById(event.id); + }; + + return ( +
+ + {/* Header */} + + + + + + 归档 + + + + + {/* Content */} + + {archivedReminders.length === 0 ? ( + + + 暂无已归档的提醒 + + + 完成的提醒会自动归档到这里 + + + ) : ( + + {archivedReminders.map((event) => ( + + + + + {event.title} + + + {new Date(event.date).toLocaleString('zh-CN', { + month: 'numeric', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} + + {event.content && ( + + {event.content} + + )} + + + handleRestore(event)} + style={{ color: '#666' }} + title="恢复" + > + + + handleDelete(event)} + style={{ color: '#999' }} + title="删除" + > + + + + + + ))} + + )} + + +
+ ); +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index b797173..98e0828 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -14,8 +14,9 @@ import { Stack, } from '@mantine/core'; import { DatePickerInput, TimeInput } from '@mantine/dates'; -import { IconLogout } from '@tabler/icons-react'; +import { IconLogout, IconSettings, IconArchive } from '@tabler/icons-react'; import { useDisclosure } from '@mantine/hooks'; +import { useNavigate } from 'react-router-dom'; import { useAppStore } from '../stores'; import { AnniversaryList } from '../components/anniversary/AnniversaryList'; import { ReminderList } from '../components/reminder/ReminderList'; @@ -24,6 +25,7 @@ import { FloatingAIChat } from '../components/ai/FloatingAIChat'; import type { Event, EventType, RepeatType } from '../types'; export function HomePage() { + const navigate = useNavigate(); const user = useAppStore((state) => state.user); const logout = useAppStore((state) => state.logout); const checkAuth = useAppStore((state) => state.checkAuth); @@ -124,10 +126,15 @@ export function HomePage() { const handleToggleComplete = async (event: Event) => { if (event.type !== 'reminder') return; - await updateEventById(event.id, { - is_completed: !event.is_completed, + // 使用当前期望的状态(取反) + const newCompleted = !event.is_completed; + const result = await updateEventById(event.id, { + is_completed: newCompleted, }); - fetchEvents(); + if (result.error) { + console.error('更新失败:', result.error); + } + // 乐观更新已处理 UI 响应,无需 fetchEvents }; const handleDelete = async (event: Event) => { @@ -183,6 +190,34 @@ export function HomePage() { 掐日子 + {/* 归档入口 - 一直显示 */} + + {/* 设置入口 */} + state.settings); + const updateSettings = useAppStore((state) => state.updateSettings); + + // 页面加载时检查登录状态 + useEffect(() => { + const isAuthenticated = useAppStore.getState().isAuthenticated; + if (!isAuthenticated) { + navigate('/login', { replace: true }); + } + }, [navigate]); + + return ( +
+ + {/* Header */} + + + + 设置 + + + + + + {/* 节假日设置 */} + + + + + + 显示节假日 + + + 在纪念日列表中显示即将到来的节假日(最近3个) + + + + updateSettings({ showHolidays: e.currentTarget.checked })} + size="sm" + color="#1a1a1a" + /> + + + + +
+ ); +} diff --git a/src/routes.tsx b/src/routes.tsx index ac8343a..422028f 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -3,6 +3,8 @@ import { LandingPage } from './pages/LandingPage'; import { LoginPage } from './pages/LoginPage'; import { RegisterPage } from './pages/RegisterPage'; import { HomePage } from './pages/HomePage'; +import { SettingsPage } from './pages/SettingsPage'; +import { ArchivePage } from './pages/ArchivePage'; import { useAppStore } from './stores'; import { useEffect, useRef } from 'react'; @@ -143,4 +145,20 @@ export const router = createBrowserRouter([ ), }, + { + path: '/settings', + element: ( + + + + ), + }, + { + path: '/archive', + element: ( + + + + ), + }, ]); diff --git a/src/stores/index.ts b/src/stores/index.ts index ffa1865..a4e1d29 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -1,14 +1,28 @@ import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; +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) => void; + // Data state events: Event[]; notes: Note | null; @@ -48,10 +62,17 @@ export const useAppStore = create()( 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 }), @@ -113,12 +134,21 @@ export const useAppStore = create()( updateEventById: async (id, event) => { try { - const updated = await api.events.update(id, event); + // 乐观更新:立即更新本地状态 set((state) => ({ - events: state.events.map((e) => (e.id === id ? updated : e)), + 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 || '更新失败' }; } }, @@ -143,7 +173,6 @@ export const useAppStore = create()( set({ user, isAuthenticated: true }); return { error: null }; } catch (error: any) { - // 确保返回字符串错误信息 const errorMessage = error.message || '登录失败,请检查邮箱和密码'; return { error: errorMessage }; } @@ -156,7 +185,6 @@ export const useAppStore = create()( set({ user, isAuthenticated: true }); return { error: null }; } catch (error: any) { - // 确保返回字符串错误信息 const errorMessage = error.message || '注册失败,请稍后重试'; return { error: errorMessage }; } @@ -201,9 +229,11 @@ export const useAppStore = create()( }), { name: 'qia-storage', + storage: createJSONStorage(() => localStorage), partialize: (state) => ({ user: state.user, isAuthenticated: state.isAuthenticated, + settings: state.settings, }), } ) diff --git a/tmpclaude-2781-cwd b/tmpclaude-2781-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-2781-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-3f83-cwd b/tmpclaude-3f83-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-3f83-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-51ef-cwd b/tmpclaude-51ef-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-51ef-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-53b7-cwd b/tmpclaude-53b7-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-53b7-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-5843-cwd b/tmpclaude-5843-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-5843-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-6e9e-cwd b/tmpclaude-6e9e-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-6e9e-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-7db7-cwd b/tmpclaude-7db7-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-7db7-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-8b3b-cwd b/tmpclaude-8b3b-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-8b3b-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-90be-cwd b/tmpclaude-90be-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-90be-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-90fc-cwd b/tmpclaude-90fc-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-90fc-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-05e0-cwd b/tmpclaude-a0eb-cwd similarity index 100% rename from tmpclaude-05e0-cwd rename to tmpclaude-a0eb-cwd diff --git a/tmpclaude-b174-cwd b/tmpclaude-b174-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-b174-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-1682-cwd b/tmpclaude-bde2-cwd similarity index 100% rename from tmpclaude-1682-cwd rename to tmpclaude-bde2-cwd diff --git a/tmpclaude-c0a2-cwd b/tmpclaude-c0a2-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-c0a2-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-ca71-cwd b/tmpclaude-ca71-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-ca71-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-d261-cwd b/tmpclaude-d261-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-d261-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-d732-cwd b/tmpclaude-d732-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-d732-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-ea09-cwd b/tmpclaude-ea09-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-ea09-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-eae0-cwd b/tmpclaude-eae0-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-eae0-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-f30f-cwd b/tmpclaude-f30f-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-f30f-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-f858-cwd b/tmpclaude-f858-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-f858-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client diff --git a/tmpclaude-fec8-cwd b/tmpclaude-fec8-cwd deleted file mode 100644 index 094e142..0000000 --- a/tmpclaude-fec8-cwd +++ /dev/null @@ -1 +0,0 @@ -/e/qia/client