feat: 实现Home页四区布局

- 实现纪念日列表组件 (AnniversaryCard, AnniversaryList)
- 实现提醒列表组件 (ReminderCard, ReminderList)
- 实现便签编辑区 (NoteEditor) 带自动保存
- 实现AI对话框 (AIChatBox) 支持自然语言解析
- 更新HomePage实现四区布局和添加/编辑弹窗
- 更新类型定义和数据Store

Co-Authored-By: Claude (MiniMax-M2.1) <noreply@anthropic.com>
This commit is contained in:
ddshi 2026-01-29 15:30:33 +08:00
parent d3de5d8598
commit 4dbf9b0bbc
9 changed files with 980 additions and 32 deletions

View File

@ -0,0 +1,168 @@
import { useState, useRef, useEffect } from 'react';
import {
Paper,
Text,
Stack,
Group,
TextInput,
ActionIcon,
Loader,
Box,
Button,
Card,
Badge,
} from '@mantine/core';
import { IconSend, IconSparkles } from '@tabler/icons-react';
import { api } from '../../services/api';
import { useAppStore } from '../../stores';
import type { AIConversation } from '../../types';
interface AIChatBoxProps {
onEventCreated?: () => void;
}
export function AIChatBox({ onEventCreated }: AIChatBoxProps) {
const [message, setMessage] = useState('');
const [loading, setLoading] = useState(false);
const [conversation, setConversation] = useState<AIConversation | null>(null);
const [history, setHistory] = useState<AIConversation[]>([]);
const addConversation = useAppStore((state) => state.addConversation);
const scrollRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [history]);
const handleSend = async () => {
if (!message.trim() || loading) return;
const userMessage = message.trim();
setMessage('');
setLoading(true);
try {
const result = await api.ai.parse(userMessage);
const newConversation: AIConversation = {
id: Date.now().toString(),
user_id: '',
message: userMessage,
response: result.response,
created_at: new Date().toISOString(),
};
setConversation(newConversation);
setHistory((prev) => [...prev, newConversation]);
addConversation(newConversation);
if (onEventCreated) {
onEventCreated();
}
} catch (error) {
console.error('AI parse error:', error);
} finally {
setLoading(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<Paper p="md" withBorder radius="md" h="100%">
<Stack gap="sm" h="100%">
<Group justify="space-between">
<Group gap={6}>
<IconSparkles size={16} color="orange" />
<Text fw={500} size="sm">
AI
</Text>
</Group>
</Group>
{/* Chat history */}
<Box
style={{
flex: 1,
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: '8px',
}}
>
{history.length === 0 ? (
<Stack align="center" justify="center" h="100%" gap="xs">
<IconSparkles size={32} color="gray" />
<Text size="sm" c="dimmed" ta="center">
<br />
15
</Text>
</Stack>
) : (
history.map((conv) => (
<Box key={conv.id}>
<Text size="xs" c="dimmed" mb={4}>
</Text>
<Card withBorder padding="sm" radius="sm" mb="xs">
<Text size="sm">{conv.message}</Text>
</Card>
<Text size="xs" c="blue" mb={4}>
AI
</Text>
<Card withBorder padding="sm" radius="sm" bg="blue.0">
<Text size="sm" style={{ whiteSpace: 'pre-wrap' }}>
{conv.response}
</Text>
</Card>
</Box>
))
)}
{loading && (
<Group gap={8}>
<Loader size="xs" />
<Text size="xs" c="dimmed">
AI ...
</Text>
</Group>
)}
<div ref={scrollRef} />
</Box>
{/* Input */}
<Group gap="xs">
<TextInput
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="输入你的提醒事项..."
size="xs"
style={{ flex: 1 }}
disabled={loading}
/>
<ActionIcon
size="lg"
variant="filled"
color="blue"
onClick={handleSend}
disabled={!message.trim() || loading}
>
<IconSend size={16} />
</ActionIcon>
</Group>
</Stack>
</Paper>
);
}

View File

@ -0,0 +1,58 @@
import { Card, Text, Badge, Group, ActionIcon, Stack } from '@mantine/core';
import { IconHeart, IconRepeat } from '@tabler/icons-react';
import type { Event } from '../../types';
interface AnniversaryCardProps {
event: Event;
onClick: () => void;
}
export function AnniversaryCard({ event, onClick }: AnniversaryCardProps) {
const isLunar = event.is_lunar;
const repeatType = event.repeat_type;
return (
<Card
shadow="sm"
padding="sm"
radius="md"
onClick={onClick}
style={{ cursor: 'pointer', transition: 'transform 0.2s' }}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'translateY(-2px)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
>
<Group justify="space-between" wrap="nowrap">
<Stack gap={4}>
<Group gap={6}>
<IconHeart size={14} color="pink" />
<Text fw={500} size="sm" lineClamp={1}>
{event.title}
</Text>
</Group>
<Text size="xs" c="dimmed">
{new Date(event.date).toLocaleDateString('zh-CN')}
{isLunar && ' (农历)'}
</Text>
</Stack>
<Group gap={6}>
{repeatType !== 'none' && (
<Badge
size="xs"
variant="light"
color="blue"
leftSection={<IconRepeat size={10} />}
>
{repeatType === 'yearly' ? '每年' : '每月'}
</Badge>
)}
{event.is_holiday && (
<Badge size="xs" variant="light" color="green">
</Badge>
)}
</Group>
</Group>
</Card>
);
}

View File

@ -0,0 +1,58 @@
import { Stack, Text, Paper, Group, Button } from '@mantine/core';
import { IconPlus } from '@tabler/icons-react';
import { AnniversaryCard } from './AnniversaryCard';
import type { Event } from '../../types';
interface AnniversaryListProps {
events: Event[];
onEventClick: (event: Event) => void;
onAddClick: () => void;
}
export function AnniversaryList({ events, onEventClick, onAddClick }: AnniversaryListProps) {
const anniversaries = events.filter((e) => e.type === 'anniversary');
if (anniversaries.length === 0) {
return (
<Paper p="md" withBorder radius="md" h="100%">
<Stack align="center" justify="center" h="100%">
<Text c="dimmed" size="sm">
</Text>
<Button
variant="light"
size="xs"
leftSection={<IconPlus size={14} />}
onClick={onAddClick}
>
</Button>
</Stack>
</Paper>
);
}
return (
<Paper p="md" withBorder radius="md" h="100%">
<Group justify="space-between" mb="sm">
<Text fw={500} size="sm">
</Text>
<Button
variant="subtle"
size="xs"
leftSection={<IconPlus size={14} />}
onClick={onAddClick}
>
</Button>
</Group>
<Stack gap="xs" style={{ maxHeight: 'calc(100% - 40px)', overflowY: 'auto' }}>
{anniversaries.map((event) => (
<AnniversaryCard key={event.id} event={event} onClick={() => onEventClick(event)} />
))}
</Stack>
</Paper>
);
}

View File

@ -0,0 +1,99 @@
import { useState, useEffect, useCallback } from 'react';
import { Paper, Textarea, Group, Text, Stack } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { useAppStore } from '../../stores';
interface NoteEditorProps {
onSave?: () => void;
}
export function NoteEditor({ onSave }: NoteEditorProps) {
const notes = useAppStore((state) => state.notes);
const updateNotesContent = useAppStore((state) => state.updateNotesContent);
const saveNotes = useAppStore((state) => state.saveNotes);
const fetchNotes = useAppStore((state) => state.fetchNotes);
const [content, setContent] = useState('');
const [saving, setSaving] = useState(false);
const [lastSaved, setLastSaved] = useState<Date | null>(null);
// Initialize content from notes
useEffect(() => {
if (notes) {
setContent(notes.content);
}
}, [notes]);
// Fetch notes on mount
useEffect(() => {
fetchNotes();
}, [fetchNotes]);
// Auto-save with debounce
const [debouncedContent] = useDebouncedValue(content, 1000);
useEffect(() => {
if (debouncedContent !== undefined && notes) {
handleSave(debouncedContent);
}
}, [debouncedContent]);
const handleSave = useCallback(
async (value: string) => {
if (!notes) return;
setSaving(true);
try {
updateNotesContent(value);
await saveNotes(value);
setLastSaved(new Date());
} catch (error) {
console.error('Failed to save note:', error);
} finally {
setSaving(false);
}
},
[notes, updateNotesContent, saveNotes]
);
const formatLastSaved = () => {
if (!lastSaved) return '未保存';
const now = new Date();
const diff = now.getTime() - lastSaved.getTime();
if (diff < 1000) return '刚刚保存';
if (diff < 60000) return `${Math.floor(diff / 1000)}秒前保存`;
return lastSaved.toLocaleTimeString('zh-CN');
};
return (
<Paper p="md" withBorder radius="md" h="100%">
<Stack gap="sm" h="100%">
<Group justify="space-between">
<Text fw={500} size="sm">
便
</Text>
<Text size="xs" c={saving ? 'yellow' : lastSaved ? 'dimmed' : 'gray'}>
{saving ? '保存中...' : formatLastSaved()}
</Text>
</Group>
<Textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="在这里记录你的想法..."
autosize
minRows={8}
maxRows={20}
styles={{
input: {
border: 'none',
resize: 'none',
fontFamily: 'inherit',
'&:focus': { outline: 'none' },
},
}}
/>
</Stack>
</Paper>
);
}

View File

@ -0,0 +1,54 @@
import { Card, Text, Checkbox, Group, Stack } from '@mantine/core';
import type { Event } from '../../types';
interface ReminderCardProps {
event: Event;
onToggle: () => void;
onClick: () => void;
}
export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
const isCompleted = event.is_completed ?? false;
return (
<Card
shadow="sm"
padding="sm"
radius="md"
onClick={onClick}
style={{
cursor: 'pointer',
opacity: isCompleted ? 0.6 : 1,
transition: 'transform 0.2s',
textDecoration: isCompleted ? 'line-through' : 'none',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'translateY(-2px)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
>
<Group justify="space-between" wrap="nowrap">
<Stack gap={4}>
<Text fw={500} size="sm" lineClamp={1}>
{event.title}
</Text>
<Text size="xs" c="dimmed">
{new Date(event.date).toLocaleString('zh-CN')}
</Text>
{event.content && (
<Text size="xs" c="dimmed" lineClamp={1}>
{event.content}
</Text>
)}
</Stack>
<Checkbox
checked={isCompleted}
onChange={(e) => {
e.stopPropagation();
onToggle();
}}
onClick={(e) => e.stopPropagation()}
/>
</Group>
</Card>
);
}

View File

@ -0,0 +1,172 @@
import { useMemo } from 'react';
import { Stack, Text, Paper, Group, Button } from '@mantine/core';
import { IconPlus } from '@tabler/icons-react';
import { ReminderCard } from './ReminderCard';
import type { Event } from '../../types';
interface ReminderListProps {
events: Event[];
onEventClick: (event: Event) => void;
onToggleComplete: (event: Event) => void;
onAddClick: () => void;
}
export function ReminderList({
events,
onEventClick,
onToggleComplete,
onAddClick,
}: ReminderListProps) {
const grouped = useMemo(() => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const reminders = events.filter((e) => e.type === 'reminder');
const result = {
today: [] as Event[],
tomorrow: [] as Event[],
later: [] as Event[],
missed: [] as Event[],
};
reminders.forEach((event) => {
const eventDate = new Date(event.date);
if (event.is_completed) return;
if (eventDate < today) {
result.missed.push(event);
} else if (eventDate < tomorrow) {
result.today.push(event);
} else {
result.later.push(event);
}
});
// Sort by date
Object.keys(result).forEach((key) => {
result[key as keyof typeof result].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
});
return result;
}, [events]);
const hasReminders =
grouped.today.length > 0 ||
grouped.tomorrow.length > 0 ||
grouped.later.length > 0 ||
grouped.missed.length > 0;
if (!hasReminders) {
return (
<Paper p="md" withBorder radius="md" h="100%">
<Stack align="center" justify="center" h="100%">
<Text c="dimmed" size="sm">
</Text>
<Button
variant="light"
size="xs"
leftSection={<IconPlus size={14} />}
onClick={onAddClick}
>
</Button>
</Stack>
</Paper>
);
}
return (
<Paper p="md" withBorder radius="md" h="100%">
<Group justify="space-between" mb="sm">
<Text fw={500} size="sm">
</Text>
<Button
variant="subtle"
size="xs"
leftSection={<IconPlus size={14} />}
onClick={onAddClick}
>
</Button>
</Group>
<Stack gap="xs" style={{ maxHeight: 'calc(100% - 40px)', overflowY: 'auto' }}>
{/* Missed reminders */}
{grouped.missed.length > 0 && (
<>
<Text size="xs" c="red" fw={500}>
</Text>
{grouped.missed.map((event) => (
<ReminderCard
key={event.id}
event={event}
onClick={() => onEventClick(event)}
onToggle={() => onToggleComplete(event)}
/>
))}
</>
)}
{/* Today's reminders */}
{grouped.today.length > 0 && (
<>
<Text size="xs" c="blue" fw={500}>
</Text>
{grouped.today.map((event) => (
<ReminderCard
key={event.id}
event={event}
onClick={() => onEventClick(event)}
onToggle={() => onToggleComplete(event)}
/>
))}
</>
)}
{/* Tomorrow's reminders */}
{grouped.tomorrow.length > 0 && (
<>
<Text size="xs" c="teal" fw={500}>
</Text>
{grouped.tomorrow.map((event) => (
<ReminderCard
key={event.id}
event={event}
onClick={() => onEventClick(event)}
onToggle={() => onToggleComplete(event)}
/>
))}
</>
)}
{/* Later reminders */}
{grouped.later.length > 0 && (
<>
<Text size="xs" c="gray" fw={500}>
</Text>
{grouped.later.map((event) => (
<ReminderCard
key={event.id}
event={event}
onClick={() => onEventClick(event)}
onToggle={() => onToggleComplete(event)}
/>
))}
</>
)}
</Stack>
</Paper>
);
}

View File

@ -1,36 +1,164 @@
import { useEffect } from 'react';
import { Container, Grid, Title, Button, Group, Text } from '@mantine/core';
import { IconLogout } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import {
Container,
Grid,
Title,
Button,
Group,
Text,
Modal,
TextInput,
Textarea,
Switch,
Select,
Stack,
} from '@mantine/core';
import { DateInput, TimeInput } from '@mantine/dates';
import { IconLogout, IconPlus } from '@tabler/icons-react';
import { useDisclosure } from '@mantine/hooks';
import { useAppStore } from '../stores';
import { AnniversaryList } from '../components/anniversary/AnniversaryList';
import { ReminderList } from '../components/reminder/ReminderList';
import { NoteEditor } from '../components/note/NoteEditor';
import { AIChatBox } from '../components/ai/AIChatBox';
import type { Event, EventType, RepeatType } from '../types';
export function HomePage() {
const user = useAppStore((state) => state.user);
const logout = useAppStore((state) => state.logout);
const checkAuth = useAppStore((state) => state.checkAuth);
const events = useAppStore((state) => state.events);
const fetchEvents = useAppStore((state) => state.fetchEvents);
const createEvent = useAppStore((state) => state.createEvent);
const updateEventById = useAppStore((state) => state.updateEventById);
const deleteEventById = useAppStore((state) => state.deleteEventById);
const [opened, { open, close }] = useDisclosure(false);
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
const [isEdit, setIsEdit] = useState(false);
// Form state
const [formType, setFormType] = useState<EventType>('anniversary');
const [formTitle, setFormTitle] = useState('');
const [formContent, setFormContent] = useState('');
const [formDate, setFormDate] = useState<Date | null>(null);
const [formTime, setFormTime] = useState('');
const [formIsLunar, setFormIsLunar] = useState(false);
const [formRepeatType, setFormRepeatType] = useState<RepeatType>('none');
const [formIsHoliday, setFormIsHoliday] = useState(false);
useEffect(() => {
checkAuth();
}, [checkAuth]);
fetchEvents();
}, [checkAuth, fetchEvents]);
const handleLogout = async () => {
await logout();
};
const handleEventClick = (event: Event) => {
setSelectedEvent(event);
setIsEdit(true);
setFormType(event.type);
setFormTitle(event.title);
setFormContent(event.content || '');
setFormDate(new Date(event.date));
setFormIsLunar(event.is_lunar);
setFormRepeatType(event.repeat_type);
setFormIsHoliday(event.is_holiday || false);
open();
};
const handleAddClick = (type: EventType) => {
setSelectedEvent(null);
setIsEdit(false);
setFormType(type);
setFormTitle('');
setFormContent('');
setFormDate(null);
setFormTime('');
setFormIsLunar(false);
setFormRepeatType('none');
setFormIsHoliday(type === 'anniversary');
open();
};
const handleSubmit = async () => {
if (!formTitle.trim() || !formDate) return;
const dateStr = formTime
? new Date(formDate.setHours(parseInt(formTime.split(':')[0]), parseInt(formTime.split(':')[1])))
: formDate;
const eventData = {
type: formType,
title: formTitle,
content: formContent || undefined,
date: dateStr.toISOString(),
is_lunar: formIsLunar,
repeat_type: formRepeatType,
is_holiday: formIsHoliday || undefined,
};
if (isEdit && selectedEvent) {
await updateEventById(selectedEvent.id, eventData);
} else {
await createEvent(eventData);
}
close();
resetForm();
fetchEvents();
};
const handleDelete = async () => {
if (!selectedEvent) return;
await deleteEventById(selectedEvent.id);
close();
resetForm();
};
const handleToggleComplete = async (event: Event) => {
if (event.type !== 'reminder') return;
await updateEventById(event.id, {
is_completed: !event.is_completed,
});
fetchEvents();
};
const resetForm = () => {
setFormType('anniversary');
setFormTitle('');
setFormContent('');
setFormDate(null);
setFormTime('');
setFormIsLunar(false);
setFormRepeatType('none');
setFormIsHoliday(false);
setSelectedEvent(null);
setIsEdit(false);
};
const handleAIEventCreated = () => {
fetchEvents();
};
return (
<Container size="xl" py="md" style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Container size="xl" py="md" h="100vh" style={{ display: 'flex', flexDirection: 'column' }}>
{/* Header */}
<Group justify="space-between" mb="md">
<Group justify="space-between" mb="md" style={{ flexShrink: 0 }}>
<Title order={2} c="blue">
</Title>
<Group>
<Text size="sm" c="dimmed">
{user?.email}
{user?.nickname || user?.email}
</Text>
<Button
variant="subtle"
color="gray"
leftSection={<IconLogout size={16} />}
size="xs"
leftSection={<IconLogout size={14} />}
onClick={handleLogout}
>
退
@ -38,12 +166,158 @@ export function HomePage() {
</Group>
</Group>
{/* Main Content - Placeholder for now */}
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Text c="dimmed">
Home Page -
</Text>
{/* Main Content - 4 panel layout */}
<Grid grow style={{ flex: 1, minHeight: 0 }}>
{/* Left column - 40% */}
<Grid.Col span={4}>
<Stack gap="md" h="100%">
{/* Anniversary list */}
<div style={{ flex: 1, minHeight: 0 }}>
<AnniversaryList
events={events}
onEventClick={handleEventClick}
onAddClick={() => handleAddClick('anniversary')}
/>
</div>
{/* Reminder list */}
<div style={{ flex: 1, minHeight: 0 }}>
<ReminderList
events={events}
onEventClick={handleEventClick}
onToggleComplete={handleToggleComplete}
onAddClick={() => handleAddClick('reminder')}
/>
</div>
</Stack>
</Grid.Col>
{/* Right column - 60% */}
<Grid.Col span={8}>
<Stack gap="md" h="100%">
{/* Note editor */}
<div style={{ flex: 1, minHeight: 0 }}>
<NoteEditor />
</div>
{/* AI Chat */}
<div style={{ flex: 1, minHeight: 0 }}>
<AIChatBox onEventCreated={handleAIEventCreated} />
</div>
</Stack>
</Grid.Col>
</Grid>
{/* Add/Edit Event Modal */}
<Modal
opened={opened}
onClose={close}
title={
<Group gap={8}>
<IconPlus size={18} />
<Text fw={500}>{isEdit ? '编辑事件' : '添加事件'}</Text>
</Group>
}
size="md"
>
<Stack gap="md">
{/* Event type */}
<Select
label="类型"
data={[
{ value: 'anniversary', label: '纪念日' },
{ value: 'reminder', label: '提醒' },
]}
value={formType}
onChange={(value) => value && setFormType(value as EventType)}
disabled={isEdit}
/>
{/* Title */}
<TextInput
label="标题"
placeholder="输入标题"
value={formTitle}
onChange={(e) => setFormTitle(e.target.value)}
required
/>
{/* Content (only for reminders) */}
{formType === 'reminder' && (
<Textarea
label="内容"
placeholder="输入详细内容"
value={formContent}
onChange={(e) => setFormContent(e.target.value)}
rows={3}
/>
)}
{/* Date */}
<DateInput
label="日期"
placeholder="选择日期"
value={formDate}
onChange={setFormDate}
required
/>
{/* Time (only for reminders) */}
{formType === 'reminder' && (
<TimeInput
label="时间"
placeholder="选择时间"
value={formTime}
onChange={(e) => setFormTime(e.target.value)}
/>
)}
{/* Lunar switch */}
<Switch
label="农历日期"
checked={formIsLunar}
onChange={(e) => setFormIsLunar(e.currentTarget.checked)}
/>
{/* Repeat type */}
<Select
label="重复"
data={[
{ value: 'none', label: '不重复' },
{ value: 'yearly', label: '每年' },
{ value: 'monthly', label: '每月' },
]}
value={formRepeatType}
onChange={(value) => value && setFormRepeatType(value as RepeatType)}
/>
{/* Holiday switch (only for anniversaries) */}
{formType === 'anniversary' && (
<Switch
label="节假日"
checked={formIsHoliday}
onChange={(e) => setFormIsHoliday(e.currentTarget.checked)}
/>
)}
{/* Actions */}
<Group justify="space-between" mt="md">
{isEdit && (
<Button color="red" variant="light" onClick={handleDelete}>
</Button>
)}
<Group ml="auto">
<Button variant="subtle" onClick={close}>
</Button>
<Button onClick={handleSubmit} disabled={!formTitle.trim() || !formDate}>
{isEdit ? '保存' : '添加'}
</Button>
</Group>
</Group>
</Stack>
</Modal>
</Container>
);
}

View File

@ -1,6 +1,6 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { User, Event, Note, AIConversation } from '../types';
import type { User, Event, Note, AIConversation, EventType } from '../types';
import { api } from '../services/api';
interface AppState {
@ -24,6 +24,15 @@ interface AppState {
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 }>;
@ -59,6 +68,72 @@ export const useAppStore = create<AppState>()(
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.save(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 {
const updated = await api.events.update(id, event);
set((state) => ({
events: state.events.map((e) => (e.id === id ? updated : e)),
}));
return { error: null };
} catch (error: any) {
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) => {

View File

@ -12,32 +12,22 @@ export type EventType = 'anniversary' | 'reminder';
export type RepeatType = 'yearly' | 'monthly' | 'none';
export interface BaseEvent {
// Unified event type (matches backend API)
export interface Event {
id: string;
user_id: string;
type: EventType;
title: string;
date: string; // ISO date string
content?: string; // Only for reminders
date: string; // For anniversaries: date, For reminders: reminder time
is_lunar: boolean;
repeat_type: RepeatType;
is_holiday?: boolean; // Only for anniversaries
is_completed?: boolean; // Only for reminders
created_at: string;
updated_at: string;
}
export interface Anniversary extends BaseEvent {
type: 'anniversary';
is_holiday: boolean;
}
export interface Reminder extends BaseEvent {
type: 'reminder';
content: string;
reminder_time: string; // ISO datetime string
is_completed: boolean;
}
// Combined event type for lists
export type Event = Anniversary | Reminder;
// Note types
export interface Note {
id: string;