qia-client/src/pages/HomePage.tsx
ddshi 1559e603b0 feat: 实现重复提醒完成移除设置、逾期列表展开收起功能
- 重复提醒完成流程优化:
  - 勾选完成重复提醒后,自动移除repeat_type、repeat_interval、next_reminder_date
  - 自动创建下一周期的新提醒记录
  - 合并API调用,确保状态更新原子性

- 逾期列表展开/收起功能:
  - 默认收起,最多显示3条逾期提醒
  - 超过3条时显示"还有 X 个逾期提醒..."链接
  - 展开后底部显示"收起"按钮

- 时间显示优化:
  - 无时间提醒(00:00)只显示日期,不显示时间
  - 归档列表同样适用此规则

- 其他优化:
  - 归档抖动动画反馈
  - 分类折叠功能

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 13:51:38 +08:00

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>
);
}