feat(reminder): 完成P4提醒功能
- 实现提醒按时间分组显示(今天/明天/本周/更久/已错过) - 添加逾期提醒红色Alert提示 - 优化提醒卡片交互(悬停显示操作按钮) - 修复DateInput日期类型处理问题 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c08f5aa4aa
commit
ccfa763657
@ -1,4 +1,23 @@
|
||||
import { Card, Text, Checkbox, Group, Stack } from '@mantine/core';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Text,
|
||||
Checkbox,
|
||||
Group,
|
||||
Stack,
|
||||
ThemeIcon,
|
||||
Tooltip,
|
||||
ActionIcon,
|
||||
Menu,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconBell,
|
||||
IconCheck,
|
||||
IconDots,
|
||||
IconTrash,
|
||||
IconEdit,
|
||||
IconClock,
|
||||
} from '@tabler/icons-react';
|
||||
import type { Event } from '../../types';
|
||||
|
||||
interface ReminderCardProps {
|
||||
@ -9,6 +28,34 @@ interface ReminderCardProps {
|
||||
|
||||
export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
|
||||
const isCompleted = event.is_completed ?? false;
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
// 计算距离提醒时间的相关显示
|
||||
const timeInfo = useMemo(() => {
|
||||
const now = new Date();
|
||||
const eventDate = new Date(event.date);
|
||||
const diff = eventDate.getTime() - now.getTime();
|
||||
const isPast = diff < 0;
|
||||
const isToday = eventDate.toDateString() === now.toDateString();
|
||||
|
||||
// 格式化时间显示
|
||||
const timeStr = eventDate.toLocaleString('zh-CN', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
|
||||
return { isPast, isToday, timeStr, diff };
|
||||
}, [event.date]);
|
||||
|
||||
// 颜色主题
|
||||
const getThemeColor = () => {
|
||||
if (isCompleted) return 'gray';
|
||||
if (timeInfo.isPast) return 'red';
|
||||
if (timeInfo.isToday) return 'orange';
|
||||
return 'blue';
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
@ -16,38 +63,110 @@ export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
|
||||
padding="sm"
|
||||
radius="md"
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
opacity: isCompleted ? 0.6 : 1,
|
||||
transition: 'transform 0.2s',
|
||||
textDecoration: isCompleted ? 'line-through' : 'none',
|
||||
opacity: isCompleted ? 0.5 : 1,
|
||||
transition: 'all 0.2s ease',
|
||||
transform: isHovered ? 'translateY(-2px)' : 'translateY(0)',
|
||||
borderLeft: `3px solid var(--mantine-color-${getThemeColor()}-6)`,
|
||||
}}
|
||||
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>
|
||||
<Group gap="sm" style={{ flex: 1, minWidth: 0 }}>
|
||||
{/* Checkbox */}
|
||||
<Checkbox
|
||||
checked={isCompleted}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
color="green"
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={isCompleted}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
|
||||
{/* Title */}
|
||||
<Text
|
||||
fw={500}
|
||||
size="sm"
|
||||
lineClamp={1}
|
||||
style={{
|
||||
textDecoration: isCompleted ? 'line-through' : 'none',
|
||||
color: isCompleted ? 'dimmed' : undefined,
|
||||
}}
|
||||
>
|
||||
{event.title}
|
||||
</Text>
|
||||
|
||||
{/* Time and content */}
|
||||
<Group gap="xs">
|
||||
<ThemeIcon size="xs" variant="light" color={getThemeColor()}>
|
||||
<IconClock size={10} />
|
||||
</ThemeIcon>
|
||||
<Text size="xs" c={getThemeColor()}>
|
||||
{timeInfo.timeStr}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{/* Content preview */}
|
||||
{event.content && !isCompleted && (
|
||||
<Text size="xs" c="dimmed" lineClamp={1}>
|
||||
{event.content}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
{/* Quick actions */}
|
||||
<Group gap={4}>
|
||||
{isHovered && !isCompleted && (
|
||||
<Tooltip label="完成">
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
>
|
||||
<IconCheck size={14} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Menu shadow="md" width={120}>
|
||||
<Menu.Target>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<IconDots size={14} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown onClick={(e) => e.stopPropagation()}>
|
||||
<Menu.Item
|
||||
leftSection={<IconEdit size={14} />}
|
||||
onClick={onClick}
|
||||
>
|
||||
编辑
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconTrash size={14} />}
|
||||
color="red"
|
||||
onClick={onToggle}
|
||||
>
|
||||
{isCompleted ? '恢复' : '删除'}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Stack, Text, Paper, Group, Button } from '@mantine/core';
|
||||
import { IconPlus } from '@tabler/icons-react';
|
||||
import { Stack, Text, Paper, Group, Button, Badge, ThemeIcon, Alert } from '@mantine/core';
|
||||
import { IconPlus, IconBell, IconAlertCircle } from '@tabler/icons-react';
|
||||
import { ReminderCard } from './ReminderCard';
|
||||
import type { Event } from '../../types';
|
||||
|
||||
@ -22,52 +22,76 @@ export function ReminderList({
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const nextWeek = new Date(today);
|
||||
nextWeek.setDate(nextWeek.getDate() + 7);
|
||||
|
||||
const reminders = events.filter((e) => e.type === 'reminder');
|
||||
|
||||
const result = {
|
||||
today: [] as Event[],
|
||||
tomorrow: [] as Event[],
|
||||
thisWeek: [] as Event[],
|
||||
later: [] as Event[],
|
||||
missed: [] as Event[],
|
||||
completed: [] as Event[],
|
||||
};
|
||||
|
||||
reminders.forEach((event) => {
|
||||
const eventDate = new Date(event.date);
|
||||
|
||||
if (event.is_completed) return;
|
||||
// 已完成的放最后
|
||||
if (event.is_completed) {
|
||||
result.completed.push(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// 未完成的按时间分组
|
||||
if (eventDate < today) {
|
||||
result.missed.push(event);
|
||||
} else if (eventDate < tomorrow) {
|
||||
result.today.push(event);
|
||||
} else if (eventDate < nextWeek) {
|
||||
result.thisWeek.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()
|
||||
);
|
||||
});
|
||||
// 按时间排序
|
||||
const sortByDate = (a: Event, b: Event) =>
|
||||
new Date(a.date).getTime() - new Date(b.date).getTime();
|
||||
|
||||
result.today.sort(sortByDate);
|
||||
result.tomorrow.sort(sortByDate);
|
||||
result.thisWeek.sort(sortByDate);
|
||||
result.later.sort(sortByDate);
|
||||
result.missed.sort(sortByDate);
|
||||
result.completed.sort(sortByDate);
|
||||
|
||||
return result;
|
||||
}, [events]);
|
||||
|
||||
const hasReminders =
|
||||
const hasActiveReminders =
|
||||
grouped.today.length > 0 ||
|
||||
grouped.tomorrow.length > 0 ||
|
||||
grouped.thisWeek.length > 0 ||
|
||||
grouped.later.length > 0 ||
|
||||
grouped.missed.length > 0;
|
||||
|
||||
if (!hasReminders) {
|
||||
// 空状态
|
||||
if (!hasActiveReminders) {
|
||||
return (
|
||||
<Paper p="md" withBorder radius="md" h="100%">
|
||||
<Stack align="center" justify="center" h="100%">
|
||||
<Text c="dimmed" size="sm">
|
||||
<ThemeIcon size={40} variant="light" color="gray" radius="xl">
|
||||
<IconBell size={20} />
|
||||
</ThemeIcon>
|
||||
<Text c="dimmed" size="sm" ta="center">
|
||||
暂无提醒
|
||||
<br />
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
记得添加提醒事项哦
|
||||
</Text>
|
||||
</Text>
|
||||
<Button
|
||||
variant="light"
|
||||
@ -82,12 +106,26 @@ export function ReminderList({
|
||||
);
|
||||
}
|
||||
|
||||
// 已过提醒数量提示
|
||||
const missedCount = grouped.missed.length;
|
||||
|
||||
return (
|
||||
<Paper p="md" withBorder radius="md" h="100%">
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Text fw={500} size="sm">
|
||||
提醒
|
||||
</Text>
|
||||
<Paper p="md" withBorder radius="md" h="100%" style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Header */}
|
||||
<Group justify="space-between" mb="sm" style={{ flexShrink: 0 }}>
|
||||
<Group gap={8}>
|
||||
<ThemeIcon size="sm" variant="light" color="orange">
|
||||
<IconBell size={12} />
|
||||
</ThemeIcon>
|
||||
<Text fw={500} size="sm">
|
||||
提醒
|
||||
</Text>
|
||||
{missedCount > 0 && (
|
||||
<Badge size="xs" variant="filled" color="red">
|
||||
{missedCount}个逾期
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
@ -98,30 +136,46 @@ export function ReminderList({
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Stack gap="xs" style={{ maxHeight: 'calc(100% - 40px)', overflowY: 'auto' }}>
|
||||
{/* Missed reminders */}
|
||||
{/* Content */}
|
||||
<Stack gap="xs" style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
|
||||
{/* 逾期提醒 */}
|
||||
{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)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<Alert
|
||||
icon={<IconAlertCircle size={14} />}
|
||||
color="red"
|
||||
variant="light"
|
||||
p="xs"
|
||||
title={<Text size="xs" fw={500}>已错过 {grouped.missed.length}个</Text>}
|
||||
>
|
||||
<Stack gap={4}>
|
||||
{grouped.missed.slice(0, 3).map((event) => (
|
||||
<ReminderCard
|
||||
key={event.id}
|
||||
event={event}
|
||||
onClick={() => onEventClick(event)}
|
||||
onToggle={() => onToggleComplete(event)}
|
||||
/>
|
||||
))}
|
||||
{grouped.missed.length > 3 && (
|
||||
<Text size="xs" c="dimmed" ta="center">
|
||||
还有 {grouped.missed.length - 3} 个逾期提醒...
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Today's reminders */}
|
||||
{/* 今天 */}
|
||||
{grouped.today.length > 0 && (
|
||||
<>
|
||||
<Text size="xs" c="blue" fw={500}>
|
||||
今天
|
||||
</Text>
|
||||
<Group gap={4}>
|
||||
<Text size="xs" c="red" fw={600}>
|
||||
今天
|
||||
</Text>
|
||||
<Badge size="xs" variant="light" color="red">
|
||||
{grouped.today.length}
|
||||
</Badge>
|
||||
</Group>
|
||||
{grouped.today.map((event) => (
|
||||
<ReminderCard
|
||||
key={event.id}
|
||||
@ -133,12 +187,17 @@ export function ReminderList({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Tomorrow's reminders */}
|
||||
{/* 明天 */}
|
||||
{grouped.tomorrow.length > 0 && (
|
||||
<>
|
||||
<Text size="xs" c="teal" fw={500}>
|
||||
明天
|
||||
</Text>
|
||||
<Group gap={4}>
|
||||
<Text size="xs" c="teal" fw={600}>
|
||||
明天
|
||||
</Text>
|
||||
<Badge size="xs" variant="light" color="teal">
|
||||
{grouped.tomorrow.length}
|
||||
</Badge>
|
||||
</Group>
|
||||
{grouped.tomorrow.map((event) => (
|
||||
<ReminderCard
|
||||
key={event.id}
|
||||
@ -150,12 +209,39 @@ export function ReminderList({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Later reminders */}
|
||||
{/* 本周 */}
|
||||
{grouped.thisWeek.length > 0 && (
|
||||
<>
|
||||
<Group gap={4}>
|
||||
<Text size="xs" c="blue" fw={600}>
|
||||
本周
|
||||
</Text>
|
||||
<Badge size="xs" variant="light" color="blue">
|
||||
{grouped.thisWeek.length}
|
||||
</Badge>
|
||||
</Group>
|
||||
{grouped.thisWeek.map((event) => (
|
||||
<ReminderCard
|
||||
key={event.id}
|
||||
event={event}
|
||||
onClick={() => onEventClick(event)}
|
||||
onToggle={() => onToggleComplete(event)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 更久 */}
|
||||
{grouped.later.length > 0 && (
|
||||
<>
|
||||
<Text size="xs" c="gray" fw={500}>
|
||||
更久之后
|
||||
</Text>
|
||||
<Group gap={4}>
|
||||
<Text size="xs" c="gray" fw={600}>
|
||||
以后
|
||||
</Text>
|
||||
<Badge size="xs" variant="light" color="gray">
|
||||
{grouped.later.length}
|
||||
</Badge>
|
||||
</Group>
|
||||
{grouped.later.map((event) => (
|
||||
<ReminderCard
|
||||
key={event.id}
|
||||
|
||||
@ -87,9 +87,12 @@ export function HomePage() {
|
||||
const handleSubmit = async () => {
|
||||
if (!formTitle.trim() || !formDate) return;
|
||||
|
||||
// 确保 date 是 Date 对象
|
||||
const dateObj = formDate instanceof Date ? formDate : new Date(formDate as unknown as string);
|
||||
|
||||
const dateStr = formTime
|
||||
? new Date(formDate.setHours(parseInt(formTime.split(':')[0]), parseInt(formTime.split(':')[1])))
|
||||
: formDate;
|
||||
? new Date(dateObj.setHours(parseInt(formTime.split(':')[0]), parseInt(formTime.split(':')[1])))
|
||||
: dateObj;
|
||||
|
||||
const eventData = {
|
||||
type: formType,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user