feat: 优化提醒功能,修复状态保存问题
- 优化提醒分组逻辑,精确判断过期时间 - 已完成但未过期的提醒仍显示在主列表(划掉状态) - 修复 checkbox 点击事件处理 - 添加乐观更新,UI 即时响应 - 添加归档页和设置页路由 - 修复后端 is_completed 字段验证问题
This commit is contained in:
parent
250c05e85e
commit
9e4b4022bd
@ -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(() => {
|
||||
|
||||
@ -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) {
|
||||
<Paper
|
||||
p="sm"
|
||||
radius={2}
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => 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"
|
||||
/>
|
||||
|
||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }} onClick={onClick}>
|
||||
{/* Title */}
|
||||
<Text
|
||||
fw={400}
|
||||
@ -105,26 +103,16 @@ export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
|
||||
|
||||
{/* Quick actions */}
|
||||
<Group gap={4}>
|
||||
{isHovered && !isCompleted && (
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
style={{ color: '#666' }}
|
||||
>
|
||||
<IconCheck size={12} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
style={{ color: '#999' }}
|
||||
title="编辑"
|
||||
>
|
||||
<IconDots size={12} />
|
||||
</ActionIcon>
|
||||
|
||||
@ -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,20 +143,6 @@ export function ReminderList({
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
<Group gap={4}>
|
||||
{grouped.completed.length > 0 && (
|
||||
<Tooltip label="查看已归档">
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={() => setArchiveOpened(true)}
|
||||
style={{ color: '#999' }}
|
||||
>
|
||||
<IconArchive size={12} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
@ -173,7 +156,6 @@ export function ReminderList({
|
||||
添加
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* Content */}
|
||||
<Stack gap="xs" style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
|
||||
@ -245,32 +227,12 @@ export function ReminderList({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 本周 */}
|
||||
{grouped.thisWeek.length > 0 && (
|
||||
<>
|
||||
<Group gap={4}>
|
||||
<Text size="xs" c="#888" fw={400} style={{ letterSpacing: '0.05em' }}>
|
||||
本周
|
||||
</Text>
|
||||
</Group>
|
||||
{grouped.thisWeek.map((event) => (
|
||||
<ReminderCard
|
||||
key={event.id}
|
||||
event={event}
|
||||
onClick={() => onEventClick(event)}
|
||||
onToggle={() => onToggleComplete(event)}
|
||||
onDelete={onDelete ? () => onDelete(event) : undefined}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 更久 */}
|
||||
{/* 更久之后 */}
|
||||
{grouped.later.length > 0 && (
|
||||
<>
|
||||
<Group gap={4}>
|
||||
<Text size="xs" c="#999" fw={400} style={{ letterSpacing: '0.05em' }}>
|
||||
以后
|
||||
更久之后
|
||||
</Text>
|
||||
</Group>
|
||||
{grouped.later.map((event) => (
|
||||
@ -286,15 +248,6 @@ export function ReminderList({
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Archive Modal */}
|
||||
<ArchiveReminderModal
|
||||
opened={archiveOpened}
|
||||
onClose={() => setArchiveOpened(false)}
|
||||
completedReminders={grouped.completed}
|
||||
onRestore={onRestore || (() => {})}
|
||||
onDelete={onDelete || (() => {})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -199,7 +199,8 @@ export function getHolidaysForYear(year: number): Array<Holiday & { date: Date }
|
||||
for (const holiday of HOLIDAYS) {
|
||||
if (holiday.isLunar && holiday.lunarMonth && holiday.lunarDay) {
|
||||
try {
|
||||
const lunar = Lunar.fromYmd(year, holiday.lunarMonth, holiday.lunarDay);
|
||||
// 使用 Lunar 构造函数创建农历对象,再获取对应的公历日期
|
||||
const lunar = new Lunar(year, holiday.lunarMonth, holiday.lunarDay);
|
||||
const solar = lunar.getSolar();
|
||||
result.push({
|
||||
...holiday,
|
||||
|
||||
164
src/pages/ArchivePage.tsx
Normal file
164
src/pages/ArchivePage.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
Container,
|
||||
Title,
|
||||
Text,
|
||||
Stack,
|
||||
Paper,
|
||||
Group,
|
||||
Button,
|
||||
ActionIcon,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowLeft, IconRotateClockwise, IconTrash, IconArchive } from '@tabler/icons-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppStore } from '../stores';
|
||||
import type { Event } from '../types';
|
||||
|
||||
export function ArchivePage() {
|
||||
const navigate = useNavigate();
|
||||
const events = useAppStore((state) => 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 (
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
background: '#faf9f7',
|
||||
}}
|
||||
>
|
||||
<Container size="xl" py="md">
|
||||
{/* Header */}
|
||||
<Group mb="lg" style={{ flexShrink: 0 }}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="xs"
|
||||
leftSection={<IconArrowLeft size={14} />}
|
||||
onClick={() => navigate(-1)}
|
||||
style={{
|
||||
letterSpacing: '0.1em',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
<Group gap="sm">
|
||||
<IconArchive size={20} color="#666" />
|
||||
<Title
|
||||
order={2}
|
||||
style={{
|
||||
fontWeight: 300,
|
||||
fontSize: '1.25rem',
|
||||
letterSpacing: '0.15em',
|
||||
color: '#1a1a1a',
|
||||
}}
|
||||
>
|
||||
归档
|
||||
</Title>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* Content */}
|
||||
<Paper p="md" withBorder radius={4} style={{ maxWidth: 600 }}>
|
||||
{archivedReminders.length === 0 ? (
|
||||
<Stack align="center" justify="center" py="xl">
|
||||
<Text c="#999" size="sm" ta="center" style={{ letterSpacing: '0.05em' }}>
|
||||
暂无已归档的提醒
|
||||
</Text>
|
||||
<Text size="xs" c="#bbb" ta="center" mt={4}>
|
||||
完成的提醒会自动归档到这里
|
||||
</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack gap="sm">
|
||||
{archivedReminders.map((event) => (
|
||||
<Paper
|
||||
key={event.id}
|
||||
p="sm"
|
||||
radius={2}
|
||||
withBorder
|
||||
style={{
|
||||
background: 'rgba(0, 0, 0, 0.02)',
|
||||
borderColor: 'rgba(0, 0, 0, 0.06)',
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Stack gap={4} style={{ flex: 1 }}>
|
||||
<Text
|
||||
size="sm"
|
||||
fw={400}
|
||||
lineClamp={1}
|
||||
style={{
|
||||
textDecoration: 'line-through',
|
||||
color: '#999',
|
||||
letterSpacing: '0.03em',
|
||||
}}
|
||||
>
|
||||
{event.title}
|
||||
</Text>
|
||||
<Text size="xs" c="#bbb">
|
||||
{new Date(event.date).toLocaleString('zh-CN', {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</Text>
|
||||
{event.content && (
|
||||
<Text size="xs" c="#bbb" lineClamp={1}>
|
||||
{event.content}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
<Group gap={4}>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
onClick={() => handleRestore(event)}
|
||||
style={{ color: '#666' }}
|
||||
title="恢复"
|
||||
>
|
||||
<IconRotateClockwise size={14} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
onClick={() => handleDelete(event)}
|
||||
style={{ color: '#999' }}
|
||||
title="删除"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Paper>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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() {
|
||||
掐日子
|
||||
</Title>
|
||||
<Group>
|
||||
{/* 归档入口 - 一直显示 */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="xs"
|
||||
leftSection={<IconArchive size={12} />}
|
||||
onClick={() => navigate('/archive')}
|
||||
style={{
|
||||
letterSpacing: '0.1em',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
归档
|
||||
</Button>
|
||||
{/* 设置入口 */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="xs"
|
||||
leftSection={<IconSettings size={12} />}
|
||||
onClick={() => navigate('/settings')}
|
||||
style={{
|
||||
letterSpacing: '0.1em',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
设置
|
||||
</Button>
|
||||
<Text
|
||||
size="xs"
|
||||
c="#888"
|
||||
|
||||
92
src/pages/SettingsPage.tsx
Normal file
92
src/pages/SettingsPage.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
Container,
|
||||
Title,
|
||||
Text,
|
||||
Switch,
|
||||
Stack,
|
||||
Paper,
|
||||
Group,
|
||||
Button,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowLeft, IconSettings } from '@tabler/icons-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppStore } from '../stores';
|
||||
|
||||
export function SettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const settings = useAppStore((state) => state.settings);
|
||||
const updateSettings = useAppStore((state) => state.updateSettings);
|
||||
|
||||
// 页面加载时检查登录状态
|
||||
useEffect(() => {
|
||||
const isAuthenticated = useAppStore.getState().isAuthenticated;
|
||||
if (!isAuthenticated) {
|
||||
navigate('/login', { replace: true });
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
background: '#faf9f7',
|
||||
}}
|
||||
>
|
||||
<Container size="xl" py="md">
|
||||
{/* Header */}
|
||||
<Group mb="lg" style={{ flexShrink: 0 }}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="xs"
|
||||
leftSection={<IconArrowLeft size={14} />}
|
||||
onClick={() => navigate(-1)}
|
||||
style={{
|
||||
letterSpacing: '0.1em',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
<Title
|
||||
order={2}
|
||||
style={{
|
||||
fontWeight: 300,
|
||||
fontSize: '1.25rem',
|
||||
letterSpacing: '0.15em',
|
||||
color: '#1a1a1a',
|
||||
}}
|
||||
>
|
||||
设置
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper p="lg" withBorder radius={4} style={{ maxWidth: 500 }}>
|
||||
<Stack gap="lg">
|
||||
{/* 节假日设置 */}
|
||||
<Group justify="space-between">
|
||||
<Group gap="sm">
|
||||
<IconSettings size={18} color="#666" />
|
||||
<Stack gap={2}>
|
||||
<Text size="sm" fw={400} style={{ letterSpacing: '0.05em', color: '#1a1a1a' }}>
|
||||
显示节假日
|
||||
</Text>
|
||||
<Text size="xs" c="#999" style={{ letterSpacing: '0.03em' }}>
|
||||
在纪念日列表中显示即将到来的节假日(最近3个)
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Switch
|
||||
checked={settings.showHolidays}
|
||||
onChange={(e) => updateSettings({ showHolidays: e.currentTarget.checked })}
|
||||
size="sm"
|
||||
color="#1a1a1a"
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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([
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<SettingsPage />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/archive',
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<ArchivePage />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
@ -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<AppSettings>) => void;
|
||||
|
||||
// Data state
|
||||
events: Event[];
|
||||
notes: Note | null;
|
||||
@ -48,10 +62,17 @@ export const useAppStore = create<AppState>()(
|
||||
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<AppState>()(
|
||||
|
||||
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<AppState>()(
|
||||
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<AppState>()(
|
||||
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<AppState>()(
|
||||
}),
|
||||
{
|
||||
name: 'qia-storage',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: (state) => ({
|
||||
user: state.user,
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
settings: state.settings,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
@ -1 +0,0 @@
|
||||
/e/qia/client
|
||||
Loading…
x
Reference in New Issue
Block a user