- 重复提醒完成流程优化: - 勾选完成重复提醒后,自动移除repeat_type、repeat_interval、next_reminder_date - 自动创建下一周期的新提醒记录 - 合并API调用,确保状态更新原子性 - 逾期列表展开/收起功能: - 默认收起,最多显示3条逾期提醒 - 超过3条时显示"还有 X 个逾期提醒..."链接 - 展开后底部显示"收起"按钮 - 时间显示优化: - 无时间提醒(00:00)只显示日期,不显示时间 - 归档列表同样适用此规则 - 其他优化: - 归档抖动动画反馈 - 分类折叠功能 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
520 lines
16 KiB
TypeScript
520 lines
16 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import {
|
|
Container,
|
|
Title,
|
|
Button,
|
|
Group,
|
|
Text,
|
|
Modal,
|
|
TextInput,
|
|
Textarea,
|
|
Switch,
|
|
Select,
|
|
Stack,
|
|
} from '@mantine/core';
|
|
import { DatePickerInput, TimeInput } from '@mantine/dates';
|
|
import { IconLogout, IconSettings } 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';
|
|
import { NoteEditor } from '../components/note/NoteEditor';
|
|
import { FloatingAIChat } from '../components/ai/FloatingAIChat';
|
|
import type { Event, EventType, RepeatType } from '../types';
|
|
import { calculateNextReminderDate } from '../utils/repeatCalculator';
|
|
|
|
export function HomePage() {
|
|
const navigate = useNavigate();
|
|
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);
|
|
|
|
// Initialize auth and data on mount
|
|
useEffect(() => {
|
|
checkAuth();
|
|
fetchEvents().catch(console.error);
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const handleLogout = async () => {
|
|
await logout();
|
|
navigate('/landing');
|
|
};
|
|
|
|
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);
|
|
// 提取时间(如果日期中包含时间信息)
|
|
const eventDate = new Date(event.date);
|
|
const hours = eventDate.getHours();
|
|
const minutes = eventDate.getMinutes();
|
|
setFormTime(hours === 0 && minutes === 0 ? '' : `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`);
|
|
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;
|
|
|
|
// 确保 date 是 Date 对象
|
|
const dateObj = formDate instanceof Date ? formDate : new Date(formDate as unknown as string);
|
|
|
|
let dateStr: string;
|
|
const year = dateObj.getFullYear();
|
|
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
|
const day = String(dateObj.getDate()).padStart(2, '0');
|
|
|
|
if (formTime) {
|
|
// 有时间:构建本地时间格式
|
|
const hours = String(parseInt(formTime.split(':')[0])).padStart(2, '0');
|
|
const minutes = String(parseInt(formTime.split(':')[1])).padStart(2, '0');
|
|
dateStr = `${year}-${month}-${day}T${hours}:${minutes}:00`;
|
|
} else {
|
|
// 无时间:只保存日期部分
|
|
dateStr = `${year}-${month}-${day}T00:00:00`;
|
|
}
|
|
|
|
// 构建事件数据,确保不包含 undefined 值
|
|
const eventData: Record<string, any> = {
|
|
type: formType,
|
|
title: formTitle,
|
|
date: dateStr,
|
|
is_lunar: formIsLunar,
|
|
repeat_type: formRepeatType,
|
|
// 计算下一次提醒日期
|
|
next_reminder_date: calculateNextReminderDate(dateStr, formRepeatType, undefined),
|
|
is_holiday: formIsHoliday ?? false,
|
|
};
|
|
|
|
// 只有当 content 有值时才包含
|
|
if (formContent.trim()) {
|
|
eventData.content = formContent;
|
|
}
|
|
|
|
if (isEdit && selectedEvent) {
|
|
await updateEventById(selectedEvent.id, eventData);
|
|
} else {
|
|
await createEvent(eventData);
|
|
}
|
|
|
|
close();
|
|
resetForm();
|
|
fetchEvents();
|
|
};
|
|
|
|
const handleDeleteFromModal = async () => {
|
|
if (!selectedEvent) return;
|
|
await deleteEventById(selectedEvent.id);
|
|
close();
|
|
resetForm();
|
|
};
|
|
|
|
const handleToggleComplete = async (event: Event) => {
|
|
if (event.type !== 'reminder') return;
|
|
// 使用当前期望的状态(取反)
|
|
const newCompleted = !event.is_completed;
|
|
const result = await updateEventById(event.id, {
|
|
is_completed: newCompleted,
|
|
});
|
|
if (result.error) {
|
|
console.error('更新失败:', result.error);
|
|
}
|
|
// 乐观更新已处理 UI 响应,无需 fetchEvents
|
|
};
|
|
|
|
const handleDelete = async (event: Event) => {
|
|
if (event.type !== 'reminder') return;
|
|
await deleteEventById(event.id);
|
|
fetchEvents();
|
|
};
|
|
|
|
const handlePostpone = async (event: Event) => {
|
|
if (event.type !== 'reminder') return;
|
|
// 将日期顺延到今天,保留原事件的时间
|
|
const today = new Date();
|
|
const originalDate = new Date(event.date);
|
|
|
|
// 提取原时间的小时和分钟
|
|
const hours = originalDate.getHours();
|
|
const minutes = originalDate.getMinutes();
|
|
|
|
// 构建新的本地时间字符串
|
|
const newDateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}T${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
|
|
|
|
const result = await updateEventById(event.id, {
|
|
date: newDateStr,
|
|
});
|
|
if (result.error) {
|
|
console.error('顺延失败:', result.error);
|
|
}
|
|
};
|
|
|
|
const resetForm = () => {
|
|
setFormType('anniversary');
|
|
setFormTitle('');
|
|
setFormContent('');
|
|
setFormDate(null);
|
|
setFormTime('');
|
|
setFormIsLunar(false);
|
|
setFormRepeatType('none');
|
|
setFormIsHoliday(false);
|
|
setSelectedEvent(null);
|
|
setIsEdit(false);
|
|
};
|
|
|
|
const handleAIEventCreated = () => {
|
|
fetchEvents();
|
|
};
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
minHeight: '100vh',
|
|
background: '#faf9f7',
|
|
overflow: 'hidden',
|
|
height: '100vh',
|
|
}}
|
|
>
|
|
<Container size="xl" py="md" h="100vh" style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
{/* Header */}
|
|
<Group justify="space-between" mb="md" style={{ flexShrink: 0 }}>
|
|
<Title
|
|
order={2}
|
|
style={{
|
|
fontWeight: 300,
|
|
fontSize: '1.25rem',
|
|
letterSpacing: '0.15em',
|
|
color: '#1a1a1a',
|
|
}}
|
|
>
|
|
掐日子
|
|
</Title>
|
|
<Group>
|
|
{/* 设置入口 */}
|
|
<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"
|
|
style={{ letterSpacing: '0.05em' }}
|
|
>
|
|
{user?.nickname || user?.email}
|
|
</Text>
|
|
<Button
|
|
variant="subtle"
|
|
color="gray"
|
|
size="xs"
|
|
leftSection={<IconLogout size={12} />}
|
|
onClick={handleLogout}
|
|
style={{
|
|
letterSpacing: '0.1em',
|
|
borderRadius: 2,
|
|
}}
|
|
>
|
|
退出
|
|
</Button>
|
|
</Group>
|
|
</Group>
|
|
|
|
{/* Main Content - 3 column horizontal layout */}
|
|
<div style={{ flex: 1, minHeight: 0, display: 'flex', gap: 16, overflow: 'hidden' }}>
|
|
{/* Left column - Anniversary */}
|
|
<div style={{ flex: 1, minWidth: 0, height: '100%', overflow: 'hidden' }}>
|
|
<AnniversaryList
|
|
events={events}
|
|
onEventClick={handleEventClick}
|
|
onAddClick={() => handleAddClick('anniversary')}
|
|
/>
|
|
</div>
|
|
|
|
{/* Middle column - Reminder */}
|
|
<div style={{ flex: 1, minWidth: 0, height: '100%', overflow: 'hidden' }}>
|
|
<ReminderList
|
|
events={events}
|
|
onEventClick={handleEventClick}
|
|
onToggleComplete={handleToggleComplete}
|
|
onAddClick={() => handleAddClick('reminder')}
|
|
onDelete={handleDelete}
|
|
onPostpone={handlePostpone}
|
|
/>
|
|
</div>
|
|
|
|
{/* Right column - Note */}
|
|
<div style={{ flex: 1, minWidth: 0, height: '100%', overflow: 'hidden' }}>
|
|
<NoteEditor />
|
|
</div>
|
|
</div>
|
|
|
|
{/* AI Chat - Floating */}
|
|
<FloatingAIChat onEventCreated={handleAIEventCreated} />
|
|
|
|
{/* Add/Edit Event Modal */}
|
|
<Modal
|
|
opened={opened}
|
|
onClose={close}
|
|
title={
|
|
<Text
|
|
fw={400}
|
|
style={{
|
|
letterSpacing: '0.1em',
|
|
fontSize: '1rem',
|
|
}}
|
|
>
|
|
{isEdit ? '编辑事件' : '添加事件'}
|
|
</Text>
|
|
}
|
|
size="md"
|
|
styles={{
|
|
header: {
|
|
borderBottom: '1px solid rgba(0,0,0,0.06)',
|
|
},
|
|
body: {
|
|
paddingTop: 20,
|
|
},
|
|
}}
|
|
>
|
|
<Stack gap="md">
|
|
{/* Event type */}
|
|
<Select
|
|
label={
|
|
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 4 }}>
|
|
类型
|
|
</Text>
|
|
}
|
|
data={[
|
|
{ value: 'anniversary', label: '纪念日' },
|
|
{ value: 'reminder', label: '提醒' },
|
|
]}
|
|
value={formType}
|
|
onChange={(value) => value && setFormType(value as EventType)}
|
|
disabled={isEdit}
|
|
styles={{
|
|
input: {
|
|
borderRadius: 2,
|
|
background: '#faf9f7',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
{/* Title */}
|
|
<TextInput
|
|
label={
|
|
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 4 }}>
|
|
标题
|
|
</Text>
|
|
}
|
|
placeholder="输入标题"
|
|
value={formTitle}
|
|
onChange={(e) => setFormTitle(e.target.value)}
|
|
required
|
|
styles={{
|
|
input: {
|
|
borderRadius: 2,
|
|
background: '#faf9f7',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
{/* Content (only for reminders) */}
|
|
{formType === 'reminder' && (
|
|
<Textarea
|
|
label={
|
|
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 4 }}>
|
|
内容
|
|
</Text>
|
|
}
|
|
placeholder="输入详细内容"
|
|
value={formContent}
|
|
onChange={(e) => setFormContent(e.target.value)}
|
|
rows={3}
|
|
styles={{
|
|
input: {
|
|
borderRadius: 2,
|
|
background: '#faf9f7',
|
|
},
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Date */}
|
|
<DatePickerInput
|
|
label={
|
|
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 4 }}>
|
|
日期
|
|
</Text>
|
|
}
|
|
placeholder="选择日期"
|
|
value={formDate}
|
|
onChange={(value) => setFormDate(value as Date | null)}
|
|
required
|
|
styles={{
|
|
input: {
|
|
borderRadius: 2,
|
|
background: '#faf9f7',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
{/* Time (only for reminders) */}
|
|
{formType === 'reminder' && (
|
|
<TimeInput
|
|
label={
|
|
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 4 }}>
|
|
时间
|
|
</Text>
|
|
}
|
|
placeholder="选择时间"
|
|
value={formTime}
|
|
onChange={(e) => setFormTime(e.target.value)}
|
|
styles={{
|
|
input: {
|
|
borderRadius: 2,
|
|
background: '#faf9f7',
|
|
},
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Lunar switch */}
|
|
<Switch
|
|
label={
|
|
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em' }}>
|
|
农历日期
|
|
</Text>
|
|
}
|
|
checked={formIsLunar}
|
|
onChange={(e) => setFormIsLunar(e.currentTarget.checked)}
|
|
/>
|
|
|
|
{/* Repeat type */}
|
|
<Select
|
|
label={
|
|
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 4 }}>
|
|
重复
|
|
</Text>
|
|
}
|
|
data={[
|
|
{ value: 'none', label: '不重复' },
|
|
{ value: 'daily', label: '每天' },
|
|
{ value: 'weekly', label: '每周' },
|
|
{ value: 'monthly', label: '每月' },
|
|
{ value: 'yearly', label: '每年' },
|
|
]}
|
|
value={formRepeatType}
|
|
onChange={(value) => value && setFormRepeatType(value as RepeatType)}
|
|
styles={{
|
|
input: {
|
|
borderRadius: 2,
|
|
background: '#faf9f7',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
{/* Holiday switch (only for anniversaries) */}
|
|
{formType === 'anniversary' && (
|
|
<Switch
|
|
label={
|
|
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em' }}>
|
|
节假日
|
|
</Text>
|
|
}
|
|
checked={formIsHoliday}
|
|
onChange={(e) => setFormIsHoliday(e.currentTarget.checked)}
|
|
/>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<Group justify="space-between" mt="md">
|
|
{isEdit && (
|
|
<Button
|
|
color="dark"
|
|
variant="light"
|
|
onClick={handleDeleteFromModal}
|
|
style={{
|
|
borderRadius: 2,
|
|
}}
|
|
>
|
|
删除
|
|
</Button>
|
|
)}
|
|
<Group ml="auto">
|
|
<Button
|
|
variant="subtle"
|
|
onClick={close}
|
|
style={{
|
|
borderRadius: 2,
|
|
color: '#666',
|
|
}}
|
|
>
|
|
取消
|
|
</Button>
|
|
<Button
|
|
onClick={handleSubmit}
|
|
disabled={!formTitle.trim() || !formDate}
|
|
style={{
|
|
background: '#1a1a1a',
|
|
border: '1px solid #1a1a1a',
|
|
borderRadius: 2,
|
|
}}
|
|
>
|
|
{isEdit ? '保存' : '添加'}
|
|
</Button>
|
|
</Group>
|
|
</Group>
|
|
</Stack>
|
|
</Modal>
|
|
</Container>
|
|
</div>
|
|
);
|
|
}
|