From 4dbf9b0bbceb1e6c9cba048baf24217edd02c211 Mon Sep 17 00:00:00 2001 From: ddshi <8811906+ddshi@user.noreply.gitee.com> Date: Thu, 29 Jan 2026 15:30:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0Home=E9=A1=B5?= =?UTF-8?q?=E5=9B=9B=E5=8C=BA=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现纪念日列表组件 (AnniversaryCard, AnniversaryList) - 实现提醒列表组件 (ReminderCard, ReminderList) - 实现便签编辑区 (NoteEditor) 带自动保存 - 实现AI对话框 (AIChatBox) 支持自然语言解析 - 更新HomePage实现四区布局和添加/编辑弹窗 - 更新类型定义和数据Store Co-Authored-By: Claude (MiniMax-M2.1) --- src/components/ai/AIChatBox.tsx | 168 ++++++++++ .../anniversary/AnniversaryCard.tsx | 58 ++++ .../anniversary/AnniversaryList.tsx | 58 ++++ src/components/note/NoteEditor.tsx | 99 ++++++ src/components/reminder/ReminderCard.tsx | 54 ++++ src/components/reminder/ReminderList.tsx | 172 ++++++++++ src/pages/HomePage.tsx | 302 +++++++++++++++++- src/stores/index.ts | 77 ++++- src/types/index.ts | 24 +- 9 files changed, 980 insertions(+), 32 deletions(-) create mode 100644 src/components/ai/AIChatBox.tsx create mode 100644 src/components/anniversary/AnniversaryCard.tsx create mode 100644 src/components/anniversary/AnniversaryList.tsx create mode 100644 src/components/note/NoteEditor.tsx create mode 100644 src/components/reminder/ReminderCard.tsx create mode 100644 src/components/reminder/ReminderList.tsx diff --git a/src/components/ai/AIChatBox.tsx b/src/components/ai/AIChatBox.tsx new file mode 100644 index 0000000..67c0704 --- /dev/null +++ b/src/components/ai/AIChatBox.tsx @@ -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(null); + const [history, setHistory] = useState([]); + const addConversation = useAppStore((state) => state.addConversation); + const scrollRef = useRef(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 ( + + + + + + + AI 助手 + + + + + {/* Chat history */} + + {history.length === 0 ? ( + + + + 告诉我你想记住的事情 +
+ 例如:下周三见客户、每月15号还房贷 +
+
+ ) : ( + history.map((conv) => ( + + + 你 + + + {conv.message} + + + + AI + + + + {conv.response} + + + + )) + )} + + {loading && ( + + + + AI 正在思考... + + + )} + +
+ + + {/* Input */} + + setMessage(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="输入你的提醒事项..." + size="xs" + style={{ flex: 1 }} + disabled={loading} + /> + + + + + + + ); +} diff --git a/src/components/anniversary/AnniversaryCard.tsx b/src/components/anniversary/AnniversaryCard.tsx new file mode 100644 index 0000000..f733cb0 --- /dev/null +++ b/src/components/anniversary/AnniversaryCard.tsx @@ -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 ( + (e.currentTarget.style.transform = 'translateY(-2px)')} + onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')} + > + + + + + + {event.title} + + + + {new Date(event.date).toLocaleDateString('zh-CN')} + {isLunar && ' (农历)'} + + + + + {repeatType !== 'none' && ( + } + > + {repeatType === 'yearly' ? '每年' : '每月'} + + )} + {event.is_holiday && ( + + 节假日 + + )} + + + + ); +} diff --git a/src/components/anniversary/AnniversaryList.tsx b/src/components/anniversary/AnniversaryList.tsx new file mode 100644 index 0000000..4e806e0 --- /dev/null +++ b/src/components/anniversary/AnniversaryList.tsx @@ -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 ( + + + + 暂无纪念日 + + + + + ); + } + + return ( + + + + 纪念日 + + + + + + {anniversaries.map((event) => ( + onEventClick(event)} /> + ))} + + + ); +} diff --git a/src/components/note/NoteEditor.tsx b/src/components/note/NoteEditor.tsx new file mode 100644 index 0000000..b78b3e2 --- /dev/null +++ b/src/components/note/NoteEditor.tsx @@ -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(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 ( + + + + + 便签 + + + {saving ? '保存中...' : formatLastSaved()} + + + +