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';
|
import type { Event } from '../../types';
|
||||||
|
|
||||||
interface ReminderCardProps {
|
interface ReminderCardProps {
|
||||||
@ -9,6 +28,34 @@ interface ReminderCardProps {
|
|||||||
|
|
||||||
export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
|
export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
|
||||||
const isCompleted = event.is_completed ?? false;
|
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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@ -16,38 +63,110 @@ export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
|
|||||||
padding="sm"
|
padding="sm"
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
opacity: isCompleted ? 0.6 : 1,
|
opacity: isCompleted ? 0.5 : 1,
|
||||||
transition: 'transform 0.2s',
|
transition: 'all 0.2s ease',
|
||||||
textDecoration: isCompleted ? 'line-through' : 'none',
|
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">
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<Stack gap={4}>
|
<Group gap="sm" style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Text fw={500} size="sm" lineClamp={1}>
|
{/* Checkbox */}
|
||||||
{event.title}
|
<Checkbox
|
||||||
</Text>
|
checked={isCompleted}
|
||||||
<Text size="xs" c="dimmed">
|
onChange={(e) => {
|
||||||
{new Date(event.date).toLocaleString('zh-CN')}
|
e.stopPropagation();
|
||||||
</Text>
|
onToggle();
|
||||||
{event.content && (
|
}}
|
||||||
<Text size="xs" c="dimmed" lineClamp={1}>
|
onClick={(e) => e.stopPropagation()}
|
||||||
{event.content}
|
color="green"
|
||||||
</Text>
|
size="sm"
|
||||||
)}
|
/>
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Checkbox
|
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
|
||||||
checked={isCompleted}
|
{/* Title */}
|
||||||
onChange={(e) => {
|
<Text
|
||||||
e.stopPropagation();
|
fw={500}
|
||||||
onToggle();
|
size="sm"
|
||||||
}}
|
lineClamp={1}
|
||||||
onClick={(e) => e.stopPropagation()}
|
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>
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Stack, Text, Paper, Group, Button } from '@mantine/core';
|
import { Stack, Text, Paper, Group, Button, Badge, ThemeIcon, Alert } from '@mantine/core';
|
||||||
import { IconPlus } from '@tabler/icons-react';
|
import { IconPlus, IconBell, IconAlertCircle } from '@tabler/icons-react';
|
||||||
import { ReminderCard } from './ReminderCard';
|
import { ReminderCard } from './ReminderCard';
|
||||||
import type { Event } from '../../types';
|
import type { Event } from '../../types';
|
||||||
|
|
||||||
@ -22,52 +22,76 @@ export function ReminderList({
|
|||||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
const tomorrow = new Date(today);
|
const tomorrow = new Date(today);
|
||||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
const nextWeek = new Date(today);
|
||||||
|
nextWeek.setDate(nextWeek.getDate() + 7);
|
||||||
|
|
||||||
const reminders = events.filter((e) => e.type === 'reminder');
|
const reminders = events.filter((e) => e.type === 'reminder');
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
today: [] as Event[],
|
today: [] as Event[],
|
||||||
tomorrow: [] as Event[],
|
tomorrow: [] as Event[],
|
||||||
|
thisWeek: [] as Event[],
|
||||||
later: [] as Event[],
|
later: [] as Event[],
|
||||||
missed: [] as Event[],
|
missed: [] as Event[],
|
||||||
|
completed: [] as Event[],
|
||||||
};
|
};
|
||||||
|
|
||||||
reminders.forEach((event) => {
|
reminders.forEach((event) => {
|
||||||
const eventDate = new Date(event.date);
|
const eventDate = new Date(event.date);
|
||||||
|
|
||||||
if (event.is_completed) return;
|
// 已完成的放最后
|
||||||
|
if (event.is_completed) {
|
||||||
|
result.completed.push(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未完成的按时间分组
|
||||||
if (eventDate < today) {
|
if (eventDate < today) {
|
||||||
result.missed.push(event);
|
result.missed.push(event);
|
||||||
} else if (eventDate < tomorrow) {
|
} else if (eventDate < tomorrow) {
|
||||||
result.today.push(event);
|
result.today.push(event);
|
||||||
|
} else if (eventDate < nextWeek) {
|
||||||
|
result.thisWeek.push(event);
|
||||||
} else {
|
} else {
|
||||||
result.later.push(event);
|
result.later.push(event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort by date
|
// 按时间排序
|
||||||
Object.keys(result).forEach((key) => {
|
const sortByDate = (a: Event, b: Event) =>
|
||||||
result[key as keyof typeof result].sort(
|
new Date(a.date).getTime() - new Date(b.date).getTime();
|
||||||
(a, b) => 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;
|
return result;
|
||||||
}, [events]);
|
}, [events]);
|
||||||
|
|
||||||
const hasReminders =
|
const hasActiveReminders =
|
||||||
grouped.today.length > 0 ||
|
grouped.today.length > 0 ||
|
||||||
grouped.tomorrow.length > 0 ||
|
grouped.tomorrow.length > 0 ||
|
||||||
|
grouped.thisWeek.length > 0 ||
|
||||||
grouped.later.length > 0 ||
|
grouped.later.length > 0 ||
|
||||||
grouped.missed.length > 0;
|
grouped.missed.length > 0;
|
||||||
|
|
||||||
if (!hasReminders) {
|
// 空状态
|
||||||
|
if (!hasActiveReminders) {
|
||||||
return (
|
return (
|
||||||
<Paper p="md" withBorder radius="md" h="100%">
|
<Paper p="md" withBorder radius="md" h="100%">
|
||||||
<Stack align="center" justify="center" 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>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
@ -82,12 +106,26 @@ export function ReminderList({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 已过提醒数量提示
|
||||||
|
const missedCount = grouped.missed.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper p="md" withBorder radius="md" h="100%">
|
<Paper p="md" withBorder radius="md" h="100%" style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<Group justify="space-between" mb="sm">
|
{/* Header */}
|
||||||
<Text fw={500} size="sm">
|
<Group justify="space-between" mb="sm" style={{ flexShrink: 0 }}>
|
||||||
提醒
|
<Group gap={8}>
|
||||||
</Text>
|
<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
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="xs"
|
size="xs"
|
||||||
@ -98,30 +136,46 @@ export function ReminderList({
|
|||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Stack gap="xs" style={{ maxHeight: 'calc(100% - 40px)', overflowY: 'auto' }}>
|
{/* Content */}
|
||||||
{/* Missed reminders */}
|
<Stack gap="xs" style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
|
||||||
|
{/* 逾期提醒 */}
|
||||||
{grouped.missed.length > 0 && (
|
{grouped.missed.length > 0 && (
|
||||||
<>
|
<Alert
|
||||||
<Text size="xs" c="red" fw={500}>
|
icon={<IconAlertCircle size={14} />}
|
||||||
已错过
|
color="red"
|
||||||
</Text>
|
variant="light"
|
||||||
{grouped.missed.map((event) => (
|
p="xs"
|
||||||
<ReminderCard
|
title={<Text size="xs" fw={500}>已错过 {grouped.missed.length}个</Text>}
|
||||||
key={event.id}
|
>
|
||||||
event={event}
|
<Stack gap={4}>
|
||||||
onClick={() => onEventClick(event)}
|
{grouped.missed.slice(0, 3).map((event) => (
|
||||||
onToggle={() => onToggleComplete(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 && (
|
{grouped.today.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Text size="xs" c="blue" fw={500}>
|
<Group gap={4}>
|
||||||
今天
|
<Text size="xs" c="red" fw={600}>
|
||||||
</Text>
|
今天
|
||||||
|
</Text>
|
||||||
|
<Badge size="xs" variant="light" color="red">
|
||||||
|
{grouped.today.length}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
{grouped.today.map((event) => (
|
{grouped.today.map((event) => (
|
||||||
<ReminderCard
|
<ReminderCard
|
||||||
key={event.id}
|
key={event.id}
|
||||||
@ -133,12 +187,17 @@ export function ReminderList({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tomorrow's reminders */}
|
{/* 明天 */}
|
||||||
{grouped.tomorrow.length > 0 && (
|
{grouped.tomorrow.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Text size="xs" c="teal" fw={500}>
|
<Group gap={4}>
|
||||||
明天
|
<Text size="xs" c="teal" fw={600}>
|
||||||
</Text>
|
明天
|
||||||
|
</Text>
|
||||||
|
<Badge size="xs" variant="light" color="teal">
|
||||||
|
{grouped.tomorrow.length}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
{grouped.tomorrow.map((event) => (
|
{grouped.tomorrow.map((event) => (
|
||||||
<ReminderCard
|
<ReminderCard
|
||||||
key={event.id}
|
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 && (
|
{grouped.later.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Text size="xs" c="gray" fw={500}>
|
<Group gap={4}>
|
||||||
更久之后
|
<Text size="xs" c="gray" fw={600}>
|
||||||
</Text>
|
以后
|
||||||
|
</Text>
|
||||||
|
<Badge size="xs" variant="light" color="gray">
|
||||||
|
{grouped.later.length}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
{grouped.later.map((event) => (
|
{grouped.later.map((event) => (
|
||||||
<ReminderCard
|
<ReminderCard
|
||||||
key={event.id}
|
key={event.id}
|
||||||
|
|||||||
@ -87,9 +87,12 @@ export function HomePage() {
|
|||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formTitle.trim() || !formDate) return;
|
if (!formTitle.trim() || !formDate) return;
|
||||||
|
|
||||||
|
// 确保 date 是 Date 对象
|
||||||
|
const dateObj = formDate instanceof Date ? formDate : new Date(formDate as unknown as string);
|
||||||
|
|
||||||
const dateStr = formTime
|
const dateStr = formTime
|
||||||
? new Date(formDate.setHours(parseInt(formTime.split(':')[0]), parseInt(formTime.split(':')[1])))
|
? new Date(dateObj.setHours(parseInt(formTime.split(':')[0]), parseInt(formTime.split(':')[1])))
|
||||||
: formDate;
|
: dateObj;
|
||||||
|
|
||||||
const eventData = {
|
const eventData = {
|
||||||
type: formType,
|
type: formType,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user