qia-client/src/pages/HomePage.tsx
ddshi 9f67ae50ed fix: 修复日期选择器样式和中文支持
- 添加 @mantine/dates 样式导入
- 添加 dayjs 中文语言包
- 使用 DatePickerInput 直接点击展开日历
- 日期时间选择器统一使用 Input 组件样式
2026-02-06 13:55:31 +08:00

645 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState, useRef } from 'react';
import {
Container,
Title,
Button,
Group,
Text,
Modal,
TextInput,
Textarea,
Switch,
Select,
Stack,
Box,
} from '@mantine/core';
import { DatePickerInput, TimeInput } from '@mantine/dates';
import { IconCalendar, IconClock, IconSettings, IconLogout } 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, PriorityType } 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);
const [formPriority, setFormPriority] = useState<PriorityType>('none');
// 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);
setFormPriority(event.priority || 'none');
// 提取时间(如果日期中包含时间信息)
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');
setFormPriority('none');
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,
priority: formPriority,
};
// 只有当 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 updateData: Record<string, any> = {
date: newDateStr,
priority: event.priority || 'none',
};
// 如果是重复提醒,同步更新 next_reminder_date
if (event.repeat_type && event.repeat_type !== 'none') {
const nextReminderDate = calculateNextReminderDate(
newDateStr,
event.repeat_type as RepeatType,
event.repeat_interval || undefined
);
updateData.next_reminder_date = nextReminderDate;
}
const result = await updateEventById(event.id, updateData);
if (result.error) {
console.error('顺延失败:', result.error);
}
};
const handleDateChange = async (
event: Event,
date: string,
repeatType: string,
repeatInterval: number | null
) => {
if (event.type !== 'reminder') return;
// 保留原事件的时间信息
const originalDate = new Date(event.date);
const hours = originalDate.getHours();
const minutes = originalDate.getMinutes();
// 构建新的本地时间字符串
const newDateStr = `${date}T${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
// 构建更新数据
const updateData: Record<string, any> = {
date: newDateStr,
priority: event.priority || 'none',
};
// 如果是重复提醒,同步更新 next_reminder_date
if (repeatType && repeatType !== 'none') {
const nextReminderDate = calculateNextReminderDate(
newDateStr,
repeatType as RepeatType,
repeatInterval || undefined
);
updateData.next_reminder_date = nextReminderDate;
}
// 合并更新
const result = await updateEventById(event.id, updateData);
if (result.error) {
console.error('日期调整失败:', result.error);
}
fetchEvents(); // 刷新列表以更新UI
};
const handlePriorityChange = async (event: Event, priority: PriorityType) => {
if (event.type !== 'reminder') return;
// 合并更新 priority 和 date确保 updated_at 被更新
const result = await updateEventById(event.id, {
priority,
date: event.date,
});
if (result.error) {
console.error('优先级设置失败:', result.error);
}
fetchEvents(); // 刷新列表以更新UI
};
const resetForm = () => {
setFormType('anniversary');
setFormTitle('');
setFormContent('');
setFormDate(null);
setFormTime('');
setFormIsLunar(false);
setFormRepeatType('none');
setFormIsHoliday(false);
setFormPriority('none');
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}
onDateChange={handleDateChange}
onPriorityChange={handlePriorityChange}
/>
</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 & Time - Combined selector */}
<Box>
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 8 }}>
</Text>
<Group gap={8} grow>
{/* Date selector */}
<DatePickerInput
placeholder="选择日期"
value={formDate}
onChange={(value) => setFormDate(value as Date | null)}
leftSection={<IconCalendar size={14} />}
leftSectionPointerEvents="none"
styles={{
input: {
borderRadius: 2,
background: '#faf9f7',
borderColor: '#ddd',
color: '#666',
height: 32,
paddingLeft: 36,
paddingRight: 8,
},
section: {
paddingLeft: 8,
},
}}
/>
{/* Time selector (only for reminders) */}
{formType === 'reminder' && (
<TimeInput
placeholder="选择时间"
value={formTime}
onChange={(e) => setFormTime(e.target.value)}
leftSection={<IconClock size={14} />}
leftSectionPointerEvents="none"
styles={{
input: {
borderRadius: 2,
background: '#faf9f7',
borderColor: '#ddd',
color: '#666',
height: 32,
paddingLeft: 36,
paddingRight: 8,
},
section: {
paddingLeft: 8,
},
}}
/>
)}
</Group>
</Box>
{/* Lunar switch (only for anniversaries) */}
{formType === 'anniversary' && (
<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',
},
}}
/>
{/* Color (only for reminders) */}
{formType === 'reminder' && (
<Box>
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 8 }}>
</Text>
<Group gap={8}>
{([
{ value: 'none' as const, color: 'rgba(0, 0, 0, 0.15)' },
{ value: 'red' as const, color: '#dc2626' },
{ value: 'green' as const, color: '#16a34a' },
{ value: 'yellow' as const, color: '#ca8a04' },
].map((item) => (
<Box
key={item.value}
onClick={() => setFormPriority(item.value)}
style={{
width: 24,
height: 24,
borderRadius: '50%',
background: item.color,
border: formPriority === item.value ? '2px solid #1a1a1a' : '1px solid rgba(0, 0, 0, 0.1)',
cursor: 'pointer',
transition: 'all 0.2s ease',
transform: formPriority === item.value ? 'scale(1.1)' : 'scale(1)',
}}
/>
)))}
</Group>
</Box>
)}
{/* 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>
);
}